@inventure71/paddockjs 4.1.0 → 4.2.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 +42 -0
- package/INSTALL_AND_UPDATE.md +9 -2
- package/README.md +4 -2
- package/docs/architecture.md +2 -0
- package/docs/component_inventory.md +1 -1
- package/docs/getting-started.md +3 -0
- package/docs/index.md +4 -2
- package/docs/loading.md +141 -0
- package/docs/system_specs.md +4 -1
- package/docs/testing-reliability-guide.md +62 -0
- package/package.json +5 -7
- package/src/data/index.d.ts +64 -1
- package/src/environment/index.d.ts +75 -12
- package/src/index.d.ts +72 -6
- package/src/simulation/collisionGeometry.js +7 -9
- package/src/simulation/driver/driverController.js +2 -2
- package/src/simulation/driver/edgeRecovery.js +2 -2
- package/src/simulation/driver/racingControls.js +35 -25
- package/src/simulation/driver/racingLinePlan.js +6 -5
- package/src/simulation/driver/rejoinControls.js +3 -3
- package/src/simulation/driver/trafficScan.js +48 -22
- package/src/simulation/participants/participantInteractions.js +24 -15
- package/src/simulation/race/raceStep.js +19 -10
- package/src/simulation/race/safetyCar.js +2 -1
- package/src/simulation/rules/pitLaneSpeedingSteward.js +1 -1
- package/src/simulation/rules/rulesReview.js +19 -9
- package/src/simulation/rules/trackLimitsSteward.js +2 -2
- package/src/simulation/timing/sectorPerformance.js +11 -3
- package/src/simulation/track/spatialQueries.js +80 -11
- package/src/simulation/track/trackModel.js +10 -1
- package/src/simulation/vehicle/contactResolution.js +4 -4
- package/src/simulation/vehicle/vehicleGeometry.js +87 -49
- package/src/simulation/vehicle/vehiclePhysics.js +7 -2
- package/src/simulation/vehicle/vehicleSnapshots.js +5 -2
- package/src/simulation/vehicle/vehicleState.js +3 -2
- package/src/ui/raceCanvasTemplate.js +3 -2
- package/examples/train-basic-policy.mjs +0 -276
- package/examples/trainingData.mjs +0 -133
|
@@ -39,6 +39,69 @@ export interface PaddockProceduralTrackOptions {
|
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
export interface PaddockTrackPoint {
|
|
43
|
+
x: number;
|
|
44
|
+
y: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface PaddockDrsZone {
|
|
48
|
+
id: string;
|
|
49
|
+
startRatio: number;
|
|
50
|
+
endRatio: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface PaddockResolvedProceduralTrackOptions {
|
|
54
|
+
profile: PaddockProceduralTrackProfile;
|
|
55
|
+
length: {
|
|
56
|
+
min: number;
|
|
57
|
+
max: number;
|
|
58
|
+
};
|
|
59
|
+
startStraight: {
|
|
60
|
+
grid: number;
|
|
61
|
+
exit: number;
|
|
62
|
+
blend: number;
|
|
63
|
+
lockExtra: number;
|
|
64
|
+
};
|
|
65
|
+
pitLane: {
|
|
66
|
+
enabled: boolean;
|
|
67
|
+
};
|
|
68
|
+
shape: {
|
|
69
|
+
scale: number;
|
|
70
|
+
cornerDensity: number;
|
|
71
|
+
variation: number;
|
|
72
|
+
};
|
|
73
|
+
validation: {
|
|
74
|
+
minClearanceMultiplier: number;
|
|
75
|
+
minShapeVariation: number;
|
|
76
|
+
minNonAdjacentArcDistance: number;
|
|
77
|
+
maxLocalTurnRadians: number;
|
|
78
|
+
maxSampleHeadingDeltaRadians: number;
|
|
79
|
+
};
|
|
80
|
+
attempts: {
|
|
81
|
+
primary: number;
|
|
82
|
+
fallback: number;
|
|
83
|
+
};
|
|
84
|
+
cacheKey: string;
|
|
85
|
+
[key: string]: unknown;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface PaddockProceduralTrack {
|
|
89
|
+
name: string;
|
|
90
|
+
width: number;
|
|
91
|
+
kerbWidth: number;
|
|
92
|
+
gravelWidth: number;
|
|
93
|
+
runoffWidth: number;
|
|
94
|
+
barrierWidth: number;
|
|
95
|
+
sampleCount: number;
|
|
96
|
+
seed?: number;
|
|
97
|
+
curveInterpolation?: string;
|
|
98
|
+
centerlineControls?: readonly PaddockTrackPoint[];
|
|
99
|
+
drsZones?: readonly PaddockDrsZone[] | null;
|
|
100
|
+
pitLane?: { enabled?: boolean } | null;
|
|
101
|
+
generationOptions?: PaddockResolvedProceduralTrackOptions;
|
|
102
|
+
[key: string]: unknown;
|
|
103
|
+
}
|
|
104
|
+
|
|
42
105
|
export type PaddockStabilityState = 'stable' | 'understeer' | 'oversteer' | 'spin-risk' | 'destroyed';
|
|
43
106
|
export type PaddockPitIntent = 0 | 1 | 2;
|
|
44
107
|
export type PaddockScenarioPreset = 'cornering' | 'off-track-recovery' | 'overtaking-pack' | 'pit-entry';
|
|
@@ -674,8 +737,8 @@ export interface PaddockDriverControllerContext {
|
|
|
674
737
|
controlledDrivers: string[];
|
|
675
738
|
actionSpec: PaddockActionSpec;
|
|
676
739
|
observationSpec: PaddockObservationSpec;
|
|
677
|
-
result: PaddockEnvironmentResult
|
|
678
|
-
previousResult: PaddockEnvironmentResult |
|
|
740
|
+
result: PaddockEnvironmentResult;
|
|
741
|
+
previousResult: PaddockEnvironmentResult | null;
|
|
679
742
|
observation: Record<string, PaddockDriverObservation>;
|
|
680
743
|
orderedObservations: Array<{
|
|
681
744
|
driverId: string;
|
|
@@ -685,7 +748,7 @@ export interface PaddockDriverControllerContext {
|
|
|
685
748
|
}>;
|
|
686
749
|
metrics: Record<string, PaddockEnvironmentDriverMetrics>;
|
|
687
750
|
events: RaceEvent[];
|
|
688
|
-
info: PaddockEnvironmentResult['info'] |
|
|
751
|
+
info: PaddockEnvironmentResult['info'] | null;
|
|
689
752
|
previousActions: PaddockActionMap;
|
|
690
753
|
orderedPreviousActions: Array<PaddockAction | null>;
|
|
691
754
|
actions: PaddockActionMap | null;
|
|
@@ -721,14 +784,14 @@ export interface PaddockDriverController {
|
|
|
721
784
|
}
|
|
722
785
|
|
|
723
786
|
export interface PaddockControllerRuntime {
|
|
724
|
-
reset(options?: unknown): PaddockEnvironmentResult
|
|
725
|
-
step(actions: PaddockActionMap): PaddockEnvironmentResult
|
|
787
|
+
reset(options?: unknown): PaddockEnvironmentResult;
|
|
788
|
+
step(actions: PaddockActionMap): PaddockEnvironmentResult;
|
|
726
789
|
resetDrivers?(
|
|
727
790
|
placements: Record<string, PaddockScenarioPlacement>,
|
|
728
791
|
resultOptions?: PaddockEnvironmentResultOptions
|
|
729
|
-
): PaddockEnvironmentResult
|
|
792
|
+
): PaddockEnvironmentResult;
|
|
730
793
|
getObservation(): Record<string, PaddockDriverObservation>;
|
|
731
|
-
getState?(options?: { output?: 'full' | 'minimal' | 'none' }): PaddockEnvironmentResult['state']
|
|
794
|
+
getState?(options?: { output?: 'full' | 'minimal' | 'none' }): PaddockEnvironmentResult['state'];
|
|
732
795
|
getActionSpec(): PaddockActionSpec;
|
|
733
796
|
getObservationSpec(): PaddockObservationSpec;
|
|
734
797
|
destroy?(): void;
|
|
@@ -743,7 +806,7 @@ export interface PaddockDriverControllerLoopOptions {
|
|
|
743
806
|
}
|
|
744
807
|
|
|
745
808
|
export interface PaddockDriverControllerLoop {
|
|
746
|
-
readonly result: PaddockEnvironmentResult |
|
|
809
|
+
readonly result: PaddockEnvironmentResult | null;
|
|
747
810
|
readonly actionSpec: PaddockActionSpec | null;
|
|
748
811
|
readonly observationSpec: PaddockObservationSpec | null;
|
|
749
812
|
readonly stats: {
|
|
@@ -756,13 +819,13 @@ export interface PaddockDriverControllerLoop {
|
|
|
756
819
|
running: boolean;
|
|
757
820
|
lastError: unknown | null;
|
|
758
821
|
};
|
|
759
|
-
reset(options?: unknown): Promise<PaddockEnvironmentResult
|
|
822
|
+
reset(options?: unknown): Promise<PaddockEnvironmentResult>;
|
|
760
823
|
resetDrivers(
|
|
761
824
|
placements: Record<string, PaddockScenarioPlacement>,
|
|
762
825
|
resultOptions?: PaddockEnvironmentResultOptions
|
|
763
|
-
): Promise<PaddockEnvironmentResult
|
|
764
|
-
step(): Promise<PaddockEnvironmentResult
|
|
765
|
-
stepFrame(): Promise<PaddockEnvironmentResult
|
|
826
|
+
): Promise<PaddockEnvironmentResult>;
|
|
827
|
+
step(): Promise<PaddockEnvironmentResult>;
|
|
828
|
+
stepFrame(): Promise<PaddockEnvironmentResult>;
|
|
766
829
|
start(): void;
|
|
767
830
|
stop(): void;
|
|
768
831
|
}
|
package/src/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type {
|
|
|
2
2
|
PaddockActionSpec,
|
|
3
3
|
PaddockDriverControllerLoop,
|
|
4
4
|
PaddockDriverControllerLoopOptions,
|
|
5
|
+
PaddockEnvironmentResult,
|
|
5
6
|
PaddockEnvironmentOptions,
|
|
6
7
|
PaddockObservationSpec,
|
|
7
8
|
PaddockParticipantInteraction as EnvPaddockParticipantInteraction,
|
|
@@ -20,6 +21,8 @@ export type {
|
|
|
20
21
|
PaddockDriverControllerContext,
|
|
21
22
|
PaddockDriverControllerLoop,
|
|
22
23
|
PaddockDriverControllerLoopOptions,
|
|
24
|
+
PaddockEnvironmentResult,
|
|
25
|
+
PaddockEnvironmentSnapshotResult,
|
|
23
26
|
PaddockObservationSpec,
|
|
24
27
|
} from './environment/index.js';
|
|
25
28
|
|
|
@@ -71,6 +74,69 @@ export interface PaddockProceduralTrackOptions {
|
|
|
71
74
|
};
|
|
72
75
|
}
|
|
73
76
|
|
|
77
|
+
export interface PaddockTrackPoint {
|
|
78
|
+
x: number;
|
|
79
|
+
y: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface PaddockDrsZone {
|
|
83
|
+
id: string;
|
|
84
|
+
startRatio: number;
|
|
85
|
+
endRatio: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface PaddockResolvedProceduralTrackOptions {
|
|
89
|
+
profile: PaddockProceduralTrackProfile;
|
|
90
|
+
length: {
|
|
91
|
+
min: number;
|
|
92
|
+
max: number;
|
|
93
|
+
};
|
|
94
|
+
startStraight: {
|
|
95
|
+
grid: number;
|
|
96
|
+
exit: number;
|
|
97
|
+
blend: number;
|
|
98
|
+
lockExtra: number;
|
|
99
|
+
};
|
|
100
|
+
pitLane: {
|
|
101
|
+
enabled: boolean;
|
|
102
|
+
};
|
|
103
|
+
shape: {
|
|
104
|
+
scale: number;
|
|
105
|
+
cornerDensity: number;
|
|
106
|
+
variation: number;
|
|
107
|
+
};
|
|
108
|
+
validation: {
|
|
109
|
+
minClearanceMultiplier: number;
|
|
110
|
+
minShapeVariation: number;
|
|
111
|
+
minNonAdjacentArcDistance: number;
|
|
112
|
+
maxLocalTurnRadians: number;
|
|
113
|
+
maxSampleHeadingDeltaRadians: number;
|
|
114
|
+
};
|
|
115
|
+
attempts: {
|
|
116
|
+
primary: number;
|
|
117
|
+
fallback: number;
|
|
118
|
+
};
|
|
119
|
+
cacheKey: string;
|
|
120
|
+
[key: string]: unknown;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface PaddockProceduralTrack {
|
|
124
|
+
name: string;
|
|
125
|
+
width: number;
|
|
126
|
+
kerbWidth: number;
|
|
127
|
+
gravelWidth: number;
|
|
128
|
+
runoffWidth: number;
|
|
129
|
+
barrierWidth: number;
|
|
130
|
+
sampleCount: number;
|
|
131
|
+
seed?: number;
|
|
132
|
+
curveInterpolation?: string;
|
|
133
|
+
centerlineControls?: readonly PaddockTrackPoint[];
|
|
134
|
+
drsZones?: readonly PaddockDrsZone[] | null;
|
|
135
|
+
pitLane?: { enabled?: boolean } | null;
|
|
136
|
+
generationOptions?: PaddockResolvedProceduralTrackOptions;
|
|
137
|
+
[key: string]: unknown;
|
|
138
|
+
}
|
|
139
|
+
|
|
74
140
|
export type PaddockStabilityState = 'stable' | 'understeer' | 'oversteer' | 'spin-risk' | 'destroyed';
|
|
75
141
|
export type PaddockPitIntent = 0 | 1 | 2;
|
|
76
142
|
export type PaddockPitIntentRequest = PaddockPitIntent | {
|
|
@@ -959,14 +1025,14 @@ export interface PaddockEnvironmentResultOptions {
|
|
|
959
1025
|
}
|
|
960
1026
|
|
|
961
1027
|
export interface F1SimulatorExpertApi {
|
|
962
|
-
reset(options?: unknown):
|
|
963
|
-
step(actions: Record<string, F1SimulatorExpertAction>):
|
|
1028
|
+
reset(options?: unknown): PaddockEnvironmentResult;
|
|
1029
|
+
step(actions: Record<string, F1SimulatorExpertAction>): PaddockEnvironmentResult;
|
|
964
1030
|
resetDrivers?(
|
|
965
1031
|
placements: Record<string, PaddockScenarioPlacement>,
|
|
966
1032
|
resultOptions?: PaddockEnvironmentResultOptions
|
|
967
|
-
):
|
|
968
|
-
getObservation():
|
|
969
|
-
getState():
|
|
1033
|
+
): PaddockEnvironmentResult;
|
|
1034
|
+
getObservation(): PaddockEnvironmentResult['observation'];
|
|
1035
|
+
getState(): PaddockEnvironmentResult['state'];
|
|
970
1036
|
getActionSpec(): PaddockActionSpec;
|
|
971
1037
|
getObservationSpec(): PaddockObservationSpec;
|
|
972
1038
|
attachExternalRenderer(source: PaddockExternalRendererSource): void;
|
|
@@ -1133,7 +1199,7 @@ export function simSpeedToMetersPerSecond(simSpeed: number): number;
|
|
|
1133
1199
|
|
|
1134
1200
|
export function createPaddockSimulator(options: F1SimulatorOptions): PaddockSimulatorController;
|
|
1135
1201
|
export function mountF1Simulator(root: Element, options: F1SimulatorOptions): Promise<F1MountedSimulator>;
|
|
1136
|
-
export function createProceduralTrack(seed?: number | string, options?: PaddockProceduralTrackOptions):
|
|
1202
|
+
export function createProceduralTrack(seed?: number | string, options?: PaddockProceduralTrackOptions): PaddockProceduralTrack;
|
|
1137
1203
|
export function createPaddockDriverControllerLoop(options: PaddockDriverControllerLoopOptions): PaddockDriverControllerLoop;
|
|
1138
1204
|
|
|
1139
1205
|
export function mountRaceControls<T extends Element>(root: T, simulator: PaddockSimulatorController): T;
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
getVehicleGeometryState,
|
|
3
|
-
vehicleAxes,
|
|
4
|
-
} from './vehicle/vehicleGeometry.js';
|
|
1
|
+
import { getVehicleGeometryState } from './vehicle/vehicleGeometry.js';
|
|
5
2
|
import { normalizeAngle } from './simMath.js';
|
|
6
3
|
|
|
7
4
|
const DEFAULT_SWEEP_STEPS = 16;
|
|
@@ -340,17 +337,18 @@ function writeInterpolatedBodyShape(target, geometryState, amount) {
|
|
|
340
337
|
const x = pose.previousX + (pose.x - pose.previousX) * amount;
|
|
341
338
|
const y = pose.previousY + (pose.y - pose.previousY) * amount;
|
|
342
339
|
const heading = normalizeAngle(pose.previousHeading + headingDelta * amount);
|
|
343
|
-
const
|
|
340
|
+
const cos = Math.cos(heading);
|
|
341
|
+
const sin = Math.sin(heading);
|
|
344
342
|
const halfLength = target.halfLength;
|
|
345
343
|
const halfWidth = target.halfWidth;
|
|
346
344
|
|
|
347
345
|
target.center.x = x;
|
|
348
346
|
target.center.y = y;
|
|
349
347
|
target.heading = heading;
|
|
350
|
-
target.forward.x =
|
|
351
|
-
target.forward.y =
|
|
352
|
-
target.right.x =
|
|
353
|
-
target.right.y =
|
|
348
|
+
target.forward.x = cos;
|
|
349
|
+
target.forward.y = sin;
|
|
350
|
+
target.right.x = -sin;
|
|
351
|
+
target.right.y = cos;
|
|
354
352
|
|
|
355
353
|
writeOffsetCorner(target.corners[0], target.center, target.forward, target.right, halfLength, halfWidth);
|
|
356
354
|
writeOffsetCorner(target.corners[1], target.center, target.forward, target.right, halfLength, -halfWidth);
|
|
@@ -4,7 +4,7 @@ import { decideRacingControls } from './racingControls.js';
|
|
|
4
4
|
import { decideRejoinControls } from './rejoinControls.js';
|
|
5
5
|
import { shouldContinueRejoinRecovery } from './edgeRecovery.js';
|
|
6
6
|
import { decideSafetyCarControls } from './safetyCarControls.js';
|
|
7
|
-
import { VEHICLE_LIMITS } from '../vehicle/vehiclePhysics.js';
|
|
7
|
+
import { VEHICLE_LIMITS, isSimulatorPhysicsMode } from '../vehicle/vehiclePhysics.js';
|
|
8
8
|
|
|
9
9
|
export function decideDriverControls({ car, orderIndex, race }) {
|
|
10
10
|
if (car.manualControls) return car.manualControls;
|
|
@@ -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 (!isSimulatorPhysicsMode(race.physicsMode)) 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;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { clamp, normalizeAngle } from '../simMath.js';
|
|
2
2
|
import { metersToSimUnits } from '../units.js';
|
|
3
3
|
import { pointAt } from '../track/trackModel.js';
|
|
4
|
-
import { VEHICLE_LIMITS } from '../vehicle/vehiclePhysics.js';
|
|
4
|
+
import { VEHICLE_LIMITS, isSimulatorPhysicsMode } 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 = isSimulatorPhysicsMode(race.physicsMode);
|
|
9
9
|
const trackLimit = race.track.width / 2;
|
|
10
10
|
const halfCarWidth = VEHICLE_LIMITS.carWidth / 2;
|
|
11
11
|
const wholeCarOutsideLimit = trackLimit + halfCarWidth;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { clamp, normalizeAngle } from '../simMath.js';
|
|
2
2
|
import { kphToSimSpeed, metersToSimUnits, simUnitsToMeters } from '../units.js';
|
|
3
|
-
import {
|
|
4
|
-
import { VEHICLE_LIMITS } from '../vehicle/vehiclePhysics.js';
|
|
3
|
+
import { pointAtInto, sampleHeadingAt, sampleHeadingCurvatureAtInto } from '../track/trackModel.js';
|
|
4
|
+
import { VEHICLE_LIMITS, isSimulatorPhysicsMode } from '../vehicle/vehiclePhysics.js';
|
|
5
5
|
import { BRAKING_LOOKAHEAD_MAX_DISTANCE, CURVATURE_LOOKAHEAD_SAMPLES, EDGE_RECOVERY_MIN_LOOKAHEAD, LOOKAHEAD_BASE_DISTANCE, LOOKAHEAD_MAX_DISTANCE, OVERTAKE_LATERAL_MIN, PREVIEW_CURVATURE_CAP, TRAFFIC_GAP_AHEAD, TRAFFIC_SIDE_GAP } from './driverControlConstants.js';
|
|
6
6
|
import { createDriverInput } from './driverInput.js';
|
|
7
7
|
import { angleToPoint } from './driverMath.js';
|
|
@@ -10,15 +10,14 @@ 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 (isSimulatorPhysicsMode(race.physicsMode)) {
|
|
14
14
|
return decideSimulatorRacingControls(car, orderIndex, race);
|
|
15
15
|
}
|
|
16
16
|
return decideArcadeRacingControls(car, orderIndex, race);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
simulatorMode: false,
|
|
19
|
+
const ARCADE_CONTROL_PROFILE = {
|
|
20
|
+
simulatorMode: false,
|
|
22
21
|
baseLookaheadSpeedFactor: (aggression) => 1.62 - aggression * 0.12,
|
|
23
22
|
previewLookaheadSpeedFactor: 3.15,
|
|
24
23
|
edgeLookaheadPenaltyMeters: 18,
|
|
@@ -73,12 +72,14 @@ export function decideArcadeRacingControls(car, orderIndex, race) {
|
|
|
73
72
|
recoveryHeadingThrottleStart: 0.38,
|
|
74
73
|
throttleResponseKph: 16,
|
|
75
74
|
throttleScale: () => 1,
|
|
76
|
-
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export function decideArcadeRacingControls(car, orderIndex, race) {
|
|
78
|
+
return decideRacingControlsForMode(car, orderIndex, race, ARCADE_CONTROL_PROFILE);
|
|
77
79
|
}
|
|
78
80
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
simulatorMode: true,
|
|
81
|
+
const SIMULATOR_CONTROL_PROFILE = {
|
|
82
|
+
simulatorMode: true,
|
|
82
83
|
baseLookaheadSpeedFactor: (aggression) => 2.45 - aggression * 0.06,
|
|
83
84
|
previewLookaheadSpeedFactor: 6.15,
|
|
84
85
|
edgeLookaheadPenaltyMeters: 34,
|
|
@@ -141,7 +142,10 @@ export function decideSimulatorRacingControls(car, orderIndex, race) {
|
|
|
141
142
|
0,
|
|
142
143
|
1,
|
|
143
144
|
),
|
|
144
|
-
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export function decideSimulatorRacingControls(car, orderIndex, race) {
|
|
148
|
+
return decideRacingControlsForMode(car, orderIndex, race, SIMULATOR_CONTROL_PROFILE);
|
|
145
149
|
}
|
|
146
150
|
|
|
147
151
|
function decideRacingControlsForMode(car, orderIndex, race, profile) {
|
|
@@ -170,7 +174,7 @@ function decideRacingControlsForMode(car, orderIndex, race, profile) {
|
|
|
170
174
|
EDGE_RECOVERY_MIN_LOOKAHEAD,
|
|
171
175
|
LOOKAHEAD_MAX_DISTANCE,
|
|
172
176
|
);
|
|
173
|
-
const targetBase =
|
|
177
|
+
const targetBase = pointAtInto(race.track, car.progress + lookahead, TARGET_BASE_SCRATCH);
|
|
174
178
|
const curvature = Math.max(
|
|
175
179
|
car.trackState.curvature,
|
|
176
180
|
targetBase.curvature,
|
|
@@ -189,8 +193,9 @@ function decideRacingControlsForMode(car, orderIndex, race, profile) {
|
|
|
189
193
|
-edgeGuard.recoveryOffset,
|
|
190
194
|
edgeGuard.recoveryOffset,
|
|
191
195
|
);
|
|
192
|
-
|
|
193
|
-
|
|
196
|
+
TARGET_POINT_SCRATCH.x = targetBase.x + targetBase.normalX * targetOffset;
|
|
197
|
+
TARGET_POINT_SCRATCH.y = targetBase.y + targetBase.normalY * targetOffset;
|
|
198
|
+
const angleError = angleToPoint(car, TARGET_POINT_SCRATCH);
|
|
194
199
|
const headingError = normalizeAngle(targetBase.heading - car.heading);
|
|
195
200
|
const lateralError = car.trackState?.signedOffset ?? 0;
|
|
196
201
|
const outwardDriftAngle = simulatorMode &&
|
|
@@ -363,15 +368,20 @@ function decideRacingControlsForMode(car, orderIndex, race, profile) {
|
|
|
363
368
|
.controls();
|
|
364
369
|
}
|
|
365
370
|
|
|
371
|
+
const LOOKAHEAD_SAMPLE_SCRATCH = { heading: 0, curvature: 0 };
|
|
372
|
+
const TARGET_BASE_SCRATCH = { x: 0, y: 0, heading: 0, normalX: 0, normalY: 0, curvature: 0, distance: 0 };
|
|
373
|
+
const TARGET_POINT_SCRATCH = { x: 0, y: 0 };
|
|
374
|
+
|
|
366
375
|
export function maxLookaheadCurvature(track, progress, lookahead) {
|
|
367
376
|
let maximum = 0;
|
|
368
|
-
|
|
377
|
+
const sample = LOOKAHEAD_SAMPLE_SCRATCH;
|
|
378
|
+
let previousHeading = sampleHeadingCurvatureAtInto(track, progress, sample).heading;
|
|
379
|
+
const segmentDistance = Math.max(1, lookahead / CURVATURE_LOOKAHEAD_SAMPLES);
|
|
369
380
|
for (let index = 1; index <= CURVATURE_LOOKAHEAD_SAMPLES; index += 1) {
|
|
370
|
-
|
|
371
|
-
const
|
|
372
|
-
const segmentCurvature = Math.abs(normalizeAngle(sample.heading - previous.heading)) / segmentDistance;
|
|
381
|
+
sampleHeadingCurvatureAtInto(track, progress + lookahead * (index / CURVATURE_LOOKAHEAD_SAMPLES), sample);
|
|
382
|
+
const segmentCurvature = Math.abs(normalizeAngle(sample.heading - previousHeading)) / segmentDistance;
|
|
373
383
|
maximum = Math.max(maximum, Math.abs(sample.curvature ?? 0), Math.min(segmentCurvature, PREVIEW_CURVATURE_CAP));
|
|
374
|
-
|
|
384
|
+
previousHeading = sample.heading;
|
|
375
385
|
}
|
|
376
386
|
return maximum;
|
|
377
387
|
}
|
|
@@ -380,16 +390,16 @@ export function calculateRacingLineOffset(car, race, lookahead, curvature, edgeG
|
|
|
380
390
|
const cornerStrength = clamp((curvature - 0.00012) / 0.00095, 0, 1);
|
|
381
391
|
if (cornerStrength <= 0) return 0;
|
|
382
392
|
|
|
383
|
-
const
|
|
384
|
-
const
|
|
385
|
-
const
|
|
386
|
-
const signedEntry = normalizeAngle(
|
|
387
|
-
const signedExit = normalizeAngle(
|
|
393
|
+
const entryHeading = sampleHeadingAt(race.track, car.progress - lookahead * 0.35);
|
|
394
|
+
const apexHeading = sampleHeadingAt(race.track, car.progress + lookahead * 0.55);
|
|
395
|
+
const exitHeading = sampleHeadingAt(race.track, car.progress + lookahead * 1.25);
|
|
396
|
+
const signedEntry = normalizeAngle(apexHeading - entryHeading);
|
|
397
|
+
const signedExit = normalizeAngle(exitHeading - apexHeading);
|
|
388
398
|
const turnDirection = Math.sign(Math.abs(signedEntry) > Math.abs(signedExit) ? signedEntry : signedExit);
|
|
389
399
|
if (turnDirection === 0) return 0;
|
|
390
400
|
|
|
391
401
|
const aggression = car.aggression ?? car.personality?.baseAggression ?? 0.5;
|
|
392
|
-
const simulatorMode = race.physicsMode
|
|
402
|
+
const simulatorMode = isSimulatorPhysicsMode(race.physicsMode);
|
|
393
403
|
const safeEdge = race.track.width / 2 - VEHICLE_LIMITS.carWidth *
|
|
394
404
|
(simulatorMode ? 2.05 - aggression * 0.08 : 1.15 - aggression * 0.12);
|
|
395
405
|
const apexOffset = turnDirection * safeEdge *
|
|
@@ -18,15 +18,16 @@ export function planRacingLine(car, orderIndex, race) {
|
|
|
18
18
|
let bestOffset = currentOffset;
|
|
19
19
|
let bestScore = -Infinity;
|
|
20
20
|
|
|
21
|
-
LANE_OFFSETS.
|
|
22
|
-
const offset = clamp(
|
|
21
|
+
for (let laneIndex = 0; laneIndex < LANE_OFFSETS.length; laneIndex += 1) {
|
|
22
|
+
const offset = clamp(LANE_OFFSETS[laneIndex], -trackLimit, trackLimit);
|
|
23
23
|
const edgeClearance = trackLimit - Math.abs(offset);
|
|
24
24
|
let score = 80;
|
|
25
25
|
score -= Math.abs(offset - preferred) * 0.18;
|
|
26
26
|
score -= Math.abs(offset - currentOffset) * (0.09 - aggression * 0.025);
|
|
27
27
|
score -= Math.max(0, LANE_EDGE_CLEARANCE_TARGET - edgeClearance) * (0.92 - aggression * 0.42);
|
|
28
28
|
|
|
29
|
-
traffic.
|
|
29
|
+
for (let trafficIndex = 0; trafficIndex < traffic.length; trafficIndex += 1) {
|
|
30
|
+
const entry = traffic[trafficIndex];
|
|
30
31
|
const lateral = Math.abs(entry.signedOffset - offset);
|
|
31
32
|
if (entry.gap > 0 && entry.gap < TRAFFIC_GAP_AHEAD) {
|
|
32
33
|
const overlapRisk = clamp(TRAFFIC_SIDE_OVERLAP - lateral, 0, TRAFFIC_SIDE_OVERLAP);
|
|
@@ -38,7 +39,7 @@ export function planRacingLine(car, orderIndex, race) {
|
|
|
38
39
|
const sideOverlapRisk = clamp(TRAFFIC_SIDE_OVERLAP - lateral, 0, TRAFFIC_SIDE_OVERLAP);
|
|
39
40
|
score -= sideOverlapRisk * (TRAFFIC_SIDE_OVERLAP + entry.gap) * 0.052 * (1 - riskTolerance * 0.22);
|
|
40
41
|
}
|
|
41
|
-
}
|
|
42
|
+
}
|
|
42
43
|
|
|
43
44
|
if (ahead && car.gapAhead < TRAFFIC_GAP_AHEAD) {
|
|
44
45
|
const passSide = attackPlan?.targetOffset ?? clamp(
|
|
@@ -73,7 +74,7 @@ export function planRacingLine(car, orderIndex, race) {
|
|
|
73
74
|
bestScore = score;
|
|
74
75
|
bestOffset = offset;
|
|
75
76
|
}
|
|
76
|
-
}
|
|
77
|
+
}
|
|
77
78
|
|
|
78
79
|
const laneChangeRate = metersToSimUnits(0.95 + car.racecraft * 0.32 + aggression * 0.78);
|
|
79
80
|
car.desiredOffset = currentOffset + clamp(bestOffset - currentOffset, -laneChangeRate, laneChangeRate);
|
|
@@ -4,11 +4,11 @@ import { offsetTrackPoint, pointAt } from '../track/trackModel.js';
|
|
|
4
4
|
import { REJOIN_LOOKAHEAD_BASE, REJOIN_LOOKAHEAD_MAX } from './driverControlConstants.js';
|
|
5
5
|
import { angleToPoint } from './driverMath.js';
|
|
6
6
|
import { createDriverInput } from './driverInput.js';
|
|
7
|
-
import { VEHICLE_LIMITS } from '../vehicle/vehiclePhysics.js';
|
|
7
|
+
import { VEHICLE_LIMITS, isSimulatorPhysicsMode } 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 (isSimulatorPhysicsMode(race.physicsMode)) {
|
|
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 = isSimulatorPhysicsMode(race.physicsMode);
|
|
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);
|
|
@@ -2,25 +2,43 @@ import { metersToSimUnits } from '../units.js';
|
|
|
2
2
|
import { DEFEND_REAR_GAP, TRAFFIC_GAP_AHEAD, TRAFFIC_SIDE_GAP, TRAFFIC_REAR_WINDOW } from './driverControlConstants.js';
|
|
3
3
|
import { VEHICLE_LIMITS } from '../vehicle/vehiclePhysics.js';
|
|
4
4
|
|
|
5
|
+
const TRAFFIC_REAR_SCAN_WINDOW = metersToSimUnits(39);
|
|
6
|
+
const TRAFFIC_FORWARD_SCAN_WINDOW = metersToSimUnits(134);
|
|
7
|
+
const TRAFFIC_BESIDE_WINDOW = metersToSimUnits(28);
|
|
8
|
+
|
|
9
|
+
// Entries are reused across calls: drivers consume the scan synchronously
|
|
10
|
+
// within a single control decision, so a module-level pool avoids re-allocating
|
|
11
|
+
// the candidate list for every car on every step.
|
|
12
|
+
const TRAFFIC_SCRATCH = [];
|
|
13
|
+
const TRAFFIC_ENTRY_POOL = [];
|
|
14
|
+
|
|
5
15
|
export function scanNearbyTraffic(car, race) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
16
|
+
const traffic = TRAFFIC_SCRATCH;
|
|
17
|
+
traffic.length = 0;
|
|
18
|
+
const cars = race.cars;
|
|
19
|
+
for (let index = 0; index < cars.length; index += 1) {
|
|
20
|
+
const other = cars[index];
|
|
21
|
+
if (other === car) continue;
|
|
22
|
+
const gap = other.raceDistance - car.raceDistance;
|
|
23
|
+
if (gap <= -TRAFFIC_REAR_SCAN_WINDOW || gap >= TRAFFIC_FORWARD_SCAN_WINDOW) continue;
|
|
24
|
+
const entry = TRAFFIC_ENTRY_POOL[traffic.length] ??= { car: null, gap: 0, signedOffset: 0, speed: 0 };
|
|
25
|
+
entry.car = other;
|
|
26
|
+
entry.gap = gap;
|
|
27
|
+
entry.signedOffset = other.trackState?.signedOffset ?? 0;
|
|
28
|
+
entry.speed = other.speed;
|
|
29
|
+
traffic.push(entry);
|
|
30
|
+
}
|
|
31
|
+
return traffic;
|
|
15
32
|
}
|
|
16
33
|
|
|
17
34
|
export function findDefensiveThreat(traffic) {
|
|
18
35
|
let closest = null;
|
|
19
36
|
|
|
20
|
-
traffic.
|
|
21
|
-
|
|
37
|
+
for (let index = 0; index < traffic.length; index += 1) {
|
|
38
|
+
const entry = traffic[index];
|
|
39
|
+
if (entry.gap >= -VEHICLE_LIMITS.carLength * 1.1 || entry.gap < -DEFEND_REAR_GAP) continue;
|
|
22
40
|
if (!closest || entry.gap > closest.gap) closest = entry;
|
|
23
|
-
}
|
|
41
|
+
}
|
|
24
42
|
|
|
25
43
|
return closest;
|
|
26
44
|
}
|
|
@@ -28,24 +46,32 @@ export function findDefensiveThreat(traffic) {
|
|
|
28
46
|
export function findLaneTrafficAhead(traffic, offset, maxDistance) {
|
|
29
47
|
let closest = null;
|
|
30
48
|
|
|
31
|
-
traffic.
|
|
32
|
-
|
|
33
|
-
if (
|
|
49
|
+
for (let index = 0; index < traffic.length; index += 1) {
|
|
50
|
+
const entry = traffic[index];
|
|
51
|
+
if (entry.gap <= 0 || entry.gap > maxDistance) continue;
|
|
52
|
+
if (Math.abs(entry.signedOffset - offset) > TRAFFIC_SIDE_GAP) continue;
|
|
34
53
|
if (!closest || entry.gap < closest.gap) closest = entry;
|
|
35
|
-
}
|
|
54
|
+
}
|
|
36
55
|
|
|
37
56
|
return closest;
|
|
38
57
|
}
|
|
39
58
|
|
|
40
59
|
export function findLaneTrafficBeside(traffic, offset) {
|
|
41
60
|
let closest = null;
|
|
61
|
+
let closestLateral = 0;
|
|
62
|
+
let closestRisk = -Infinity;
|
|
42
63
|
|
|
43
|
-
traffic.
|
|
64
|
+
for (let index = 0; index < traffic.length; index += 1) {
|
|
65
|
+
const entry = traffic[index];
|
|
44
66
|
const lateral = Math.abs(entry.signedOffset - offset);
|
|
45
|
-
if (Math.abs(entry.gap) >
|
|
46
|
-
const risk = (
|
|
47
|
-
if (!closest || risk >
|
|
48
|
-
|
|
67
|
+
if (Math.abs(entry.gap) > TRAFFIC_BESIDE_WINDOW || lateral > TRAFFIC_SIDE_GAP) continue;
|
|
68
|
+
const risk = (TRAFFIC_BESIDE_WINDOW - Math.abs(entry.gap)) + (TRAFFIC_SIDE_GAP - lateral);
|
|
69
|
+
if (!closest || risk > closestRisk) {
|
|
70
|
+
closest = entry;
|
|
71
|
+
closestLateral = lateral;
|
|
72
|
+
closestRisk = risk;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
49
75
|
|
|
50
|
-
return closest;
|
|
76
|
+
return closest ? { ...closest, lateral: closestLateral, risk: closestRisk } : null;
|
|
51
77
|
}
|
|
@@ -41,14 +41,6 @@ export const PARTICIPANT_INTERACTION_PROFILES = Object.freeze({
|
|
|
41
41
|
}),
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
-
const INTERACTION_FLAGS = Object.freeze([
|
|
45
|
-
'collidable',
|
|
46
|
-
'detectableByRays',
|
|
47
|
-
'detectableAsNearby',
|
|
48
|
-
'blocksPitLane',
|
|
49
|
-
'affectsRaceOrder',
|
|
50
|
-
]);
|
|
51
|
-
|
|
52
44
|
export function normalizeParticipantInteractions(options = {}) {
|
|
53
45
|
const defaultProfile = resolveProfileName(options.defaultProfile);
|
|
54
46
|
const defaultInteraction = { ...PARTICIPANT_INTERACTION_PROFILES[defaultProfile] };
|
|
@@ -73,11 +65,15 @@ export function attachParticipantInteractions(cars = [], options = {}) {
|
|
|
73
65
|
|
|
74
66
|
export function resolveInteraction(override = {}, fallbackProfile = 'normal') {
|
|
75
67
|
const profile = resolveProfileName(override?.profile ?? fallbackProfile);
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
68
|
+
const base = PARTICIPANT_INTERACTION_PROFILES[profile];
|
|
69
|
+
return {
|
|
70
|
+
profile,
|
|
71
|
+
collidable: override?.collidable != null ? Boolean(override.collidable) : base.collidable,
|
|
72
|
+
detectableByRays: override?.detectableByRays != null ? Boolean(override.detectableByRays) : base.detectableByRays,
|
|
73
|
+
detectableAsNearby: override?.detectableAsNearby != null ? Boolean(override.detectableAsNearby) : base.detectableAsNearby,
|
|
74
|
+
blocksPitLane: override?.blocksPitLane != null ? Boolean(override.blocksPitLane) : base.blocksPitLane,
|
|
75
|
+
affectsRaceOrder: override?.affectsRaceOrder != null ? Boolean(override.affectsRaceOrder) : base.affectsRaceOrder,
|
|
76
|
+
};
|
|
81
77
|
}
|
|
82
78
|
|
|
83
79
|
export function canCollide(first, second) {
|
|
@@ -110,8 +106,21 @@ export function affectsRaceOrder(car) {
|
|
|
110
106
|
}
|
|
111
107
|
|
|
112
108
|
export function serializeParticipantInteraction(interaction) {
|
|
113
|
-
|
|
114
|
-
|
|
109
|
+
return resolveInteraction(interaction ?? {}, interaction?.profile ?? 'normal');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const SERIALIZED_INTERACTION_SOURCE = Symbol('serializedInteractionSource');
|
|
113
|
+
const SERIALIZED_INTERACTION_VALUE = Symbol('serializedInteractionValue');
|
|
114
|
+
|
|
115
|
+
// Render snapshots are pooled and rebuilt every frame; the interaction config
|
|
116
|
+
// only changes when a new object is attached, so cache the serialized form per
|
|
117
|
+
// car keyed on the interaction reference.
|
|
118
|
+
export function serializeParticipantInteractionCached(car) {
|
|
119
|
+
if (car[SERIALIZED_INTERACTION_SOURCE] !== car.interaction || !car[SERIALIZED_INTERACTION_VALUE]) {
|
|
120
|
+
car[SERIALIZED_INTERACTION_SOURCE] = car.interaction;
|
|
121
|
+
car[SERIALIZED_INTERACTION_VALUE] = serializeParticipantInteraction(car.interaction);
|
|
122
|
+
}
|
|
123
|
+
return car[SERIALIZED_INTERACTION_VALUE];
|
|
115
124
|
}
|
|
116
125
|
|
|
117
126
|
function resolveProfileName(profile) {
|