diff --git a/data/catalog.json b/data/catalog.json index ac08882..36d235a 100644 --- a/data/catalog.json +++ b/data/catalog.json @@ -382,5 +382,148 @@ "rating": 4, "genre": "strategy", "subgenre": "rts" + }, + { + "id": "crash-team-racing", + "title": "Crash Team Racing", + "platform": "PlayStation", + "repo": "https://github.com/CTR-tools/CTR-ModSDK", + "tier": 1, + "kind": "decompilation", + "rating": 5, + "genre": "racing", + "subgenre": "kart racing" + }, + { + "id": "mario-kart-double-dash", + "title": "Mario Kart: Double Dash!!", + "platform": "GameCube", + "repo": "https://github.com/doldecomp/mkdd", + "tier": 1, + "kind": "decompilation", + "rating": 5, + "genre": "racing", + "subgenre": "kart racing" + }, + { + "id": "lego-racers", + "title": "LEGO Racers", + "platform": "PC / DOS", + "repo": "https://github.com/isledecomp/racers", + "tier": 1, + "kind": "decompilation", + "rating": 4, + "genre": "racing", + "subgenre": "arcade racing" + }, + { + "id": "carmageddon", + "title": "Carmageddon", + "platform": "PC / DOS", + "repo": "https://github.com/Dethrace-labs/dethrace", + "tier": 1, + "kind": "decompilation", + "rating": 4, + "genre": "racing", + "subgenre": "vehicular combat" + }, + { + "id": "need-for-speed-most-wanted", + "title": "Need for Speed: Most Wanted", + "platform": "PC / DOS", + "repo": "https://github.com/dbalatoni13/nfsmw", + "tier": 1, + "kind": "decompilation", + "rating": 4, + "genre": "racing", + "subgenre": "arcade racing" + }, + { + "id": "need-for-speed", + "title": "Need for Speed", + "platform": "PC / DOS", + "repo": "https://github.com/OpenNFS/OpenNFS", + "tier": 1, + "kind": "engine-reimplementation", + "rating": 4, + "genre": "racing", + "subgenre": "arcade racing" + }, + { + "id": "rage-racer", + "title": "Rage Racer", + "platform": "PlayStation", + "repo": "https://github.com/khasinski/rage-racer-decomp", + "tier": 1, + "kind": "decompilation", + "rating": 3, + "genre": "racing", + "subgenre": "arcade racing" + }, + { + "id": "grand-prix-4", + "title": "Grand Prix 4", + "platform": "PC / DOS", + "repo": "https://github.com/leighrobertabbott/decomp-grandprix4", + "tier": 1, + "kind": "decompilation", + "rating": 4, + "genre": "racing", + "subgenre": "formula racing" + }, + { + "id": "librecorsa", + "title": "LibreCorsa", + "platform": "Web / Flash", + "repo": "https://github.com/KhazP/librecorsa", + "tier": 1, + "kind": "original-source", + "rating": 2, + "genre": "racing", + "subgenre": "formula racing" + }, + { + "id": "vdrift", + "title": "VDrift", + "platform": "PC / DOS", + "repo": "https://github.com/VDrift/vdrift", + "tier": 1, + "kind": "original-source", + "rating": 3, + "genre": "racing", + "subgenre": "sim racing" + }, + { + "id": "torcs", + "title": "TORCS", + "platform": "PC / DOS", + "repo": "https://github.com/jeremybennett/torcs", + "tier": 1, + "kind": "original-source", + "rating": 4, + "genre": "racing", + "subgenre": "sim racing" + }, + { + "id": "richard-burns-rally", + "title": "Richard Burns Rally", + "platform": "PC / DOS", + "repo": "https://github.com/JodusNodus/rerbr", + "tier": 1, + "kind": "engine-reimplementation", + "rating": 4, + "genre": "racing", + "subgenre": "rally" + }, + { + "id": "nascar-98", + "title": "NASCAR 98", + "platform": "PlayStation", + "repo": "https://github.com/shohamc1/nascar98", + "tier": 1, + "kind": "decompilation", + "rating": 3, + "genre": "racing", + "subgenre": "oval racing" } ] diff --git a/data/games/carmageddon.json b/data/games/carmageddon.json new file mode 100644 index 0000000..58bd853 --- /dev/null +++ b/data/games/carmageddon.json @@ -0,0 +1,2468 @@ +{ + "version": 1, + "id": "carmageddon", + "title": "Carmageddon", + "repo": "https://github.com/Dethrace-labs/dethrace", + "sha": "166dd93cf3dd985d027095c29d236e38bd9ae5b8", + "nodes": [ + { + "id": "car-physics", + "name": "Fixed-step vehicle simulation", + "purpose": "The per-tick mechanics pass that turns accumulated forces and torques into an updated car transform, together with the rotation, translation and drag helpers it calls in sequence for every active car", + "paths": [ + "src/DETHRACE/common/car.c", + "src/DETHRACE/constants.h" + ], + "functions": [ + { + "name": "ApplyPhysicsToCars", + "path": "src/DETHRACE/common/car.c", + "lines": [ + 1293, + 1424 + ] + }, + { + "name": "MoveAndCollideCar", + "path": "src/DETHRACE/common/car.c", + "lines": [ + 1566, + 1600 + ] + }, + { + "name": "RotateCar", + "path": "src/DETHRACE/common/car.c", + "lines": [ + 2019, + 2044 + ] + }, + { + "name": "RotateCarSecondOrder", + "path": "src/DETHRACE/common/car.c", + "lines": [ + 1935, + 1968 + ] + }, + { + "name": "TranslateCar", + "path": "src/DETHRACE/common/car.c", + "lines": [ + 2774, + 2783 + ] + }, + { + "name": "ApplyTorque", + "path": "src/DETHRACE/common/car.c", + "lines": [ + 2765, + 2770 + ] + }, + { + "name": "AddDrag", + "path": "src/DETHRACE/common/car.c", + "lines": [ + 2131, + 2154 + ] + }, + { + "name": "FinishCars", + "path": "src/DETHRACE/common/car.c", + "lines": [ + 1099, + 1160 + ] + } + ], + "dependsOn": [] + }, + { + "id": "suspension", + "parent": "car-physics", + "name": "Wheel contact and suspension", + "purpose": "Ray-casting each wheel to the ground, converting the resulting compression into spring and damper forces with matching pitch and roll torques, and deriving the per-car spring rates from mass and suspension give at setup time", + "paths": [ + "src/DETHRACE/common/car.c" + ], + "functions": [ + { + "name": "CalcForce", + "path": "src/DETHRACE/common/car.c", + "lines": [ + 2192, + 2695 + ] + }, + { + "name": "SetCarSuspGiveAndHeight", + "path": "src/DETHRACE/common/car.c", + "lines": [ + 7358, + 7388 + ] + }, + { + "name": "DoBumpiness", + "path": "src/DETHRACE/common/car.c", + "lines": [ + 2158, + 2188 + ] + }, + { + "name": "MultiFindFloorInBoxM", + "path": "src/DETHRACE/common/car.c", + "lines": [ + 3829, + 3844 + ] + }, + { + "name": "FindFloorInBoxBU2", + "path": "src/DETHRACE/common/car.c", + "lines": [ + 3938, + 3979 + ] + } + ], + "dependsOn": [ + "car-physics" + ] + }, + { + "id": "tyre-model", + "parent": "car-physics", + "name": "Tyre grip budget", + "purpose": "Turning each wheel's vertical load into a friction limit, sharing that limit between cornering and driving through a friction ellipse, and flagging a wheel as slipping once its budget is exceeded", + "paths": [ + "src/DETHRACE/common/car.c", + "src/DETHRACE/constants.h" + ], + "functions": [ + { + "name": "CalcForce", + "path": "src/DETHRACE/common/car.c", + "lines": [ + 2192, + 2695 + ] + }, + { + "name": "SkidNoise", + "path": "src/DETHRACE/common/car.c", + "lines": [ + 3334, + 3387 + ] + }, + { + "name": "StopSkid", + "path": "src/DETHRACE/common/car.c", + "lines": [ + 3391, + 3403 + ] + }, + { + "name": "ScrapeNoise", + "path": "src/DETHRACE/common/car.c", + "lines": [ + 3301, + 3334 + ] + } + ], + "dependsOn": [ + "suspension" + ] + }, + { + "id": "drivetrain", + "parent": "car-physics", + "name": "Engine, gearbox and brakes", + "purpose": "Producing engine torque from throttle and engine speed, integrating engine speed from the load at the wheels, choosing a gear automatically, and building the brake force that is later split between the axles", + "paths": [ + "src/DETHRACE/common/car.c", + "src/DETHRACE/constants.h" + ], + "functions": [ + { + "name": "CalcEngineForce", + "path": "src/DETHRACE/common/car.c", + "lines": [ + 971, + 1055 + ] + }, + { + "name": "DoRevs", + "path": "src/DETHRACE/common/car.c", + "lines": [ + 2699, + 2761 + ] + } + ], + "dependsOn": [ + "car-physics" + ] + }, + { + "id": "steering", + "parent": "car-physics", + "name": "Steering input", + "purpose": "Converting digital and analogue steering input into an accumulated path curvature, clamping it to the car's handling limit, and bleeding it away again when the driver lets go", + "paths": [ + "src/DETHRACE/common/car.c" + ], + "functions": [ + { + "name": "ControlCar1", + "path": "src/DETHRACE/common/car.c", + "lines": [ + 1867, + 1898 + ] + }, + { + "name": "ControlCar2", + "path": "src/DETHRACE/common/car.c", + "lines": [ + 1678, + 1717 + ] + }, + { + "name": "ControlCar3", + "path": "src/DETHRACE/common/car.c", + "lines": [ + 1721, + 1754 + ] + }, + { + "name": "ControlCar4", + "path": "src/DETHRACE/common/car.c", + "lines": [ + 1758, + 1808 + ] + }, + { + "name": "ControlCar5", + "path": "src/DETHRACE/common/car.c", + "lines": [ + 1812, + 1863 + ] + }, + { + "name": "SteeringSelfCentre", + "path": "src/DETHRACE/common/car.c", + "lines": [ + 2048, + 2075 + ] + } + ], + "dependsOn": [ + "car-physics" + ] + }, + { + "id": "wall-collision", + "parent": "car-physics", + "name": "Car against level geometry", + "purpose": "Gathering nearby level faces, sweeping the car's box and its extra points against them, and resolving the resulting contact points with a simultaneous impulse solve plus tangential friction", + "paths": [ + "src/DETHRACE/common/car.c" + ], + "functions": [ + { + "name": "GetFacesInBox", + "path": "src/DETHRACE/common/car.c", + "lines": [ + 726, + 845 + ] + }, + { + "name": "CollideCarWithWall", + "path": "src/DETHRACE/common/car.c", + "lines": [ + 1625, + 1648 + ] + }, + { + "name": "CollCheck", + "path": "src/DETHRACE/common/car.c", + "lines": [ + 2787, + 3192 + ] + }, + { + "name": "AddCollPoint", + "path": "src/DETHRACE/common/car.c", + "lines": [ + 3654, + 3675 + ] + }, + { + "name": "AddFriction", + "path": "src/DETHRACE/common/car.c", + "lines": [ + 3196, + 3235 + ] + }, + { + "name": "FindFloorInBoxBU", + "path": "src/DETHRACE/common/car.c", + "lines": [ + 3903, + 3938 + ] + } + ], + "dependsOn": [ + "car-physics" + ] + }, + { + "id": "car-car-collision", + "name": "Car against car", + "purpose": "Testing pairs of cars for box overlap, collecting matched contact points between them, solving both bodies as one coupled impulse problem, and handing the resulting impulses to the damage and crush systems", + "paths": [ + "src/DETHRACE/common/car.c" + ], + "functions": [ + { + "name": "CrashCarsTogether", + "path": "src/DETHRACE/common/car.c", + "lines": [ + 6079, + 6101 + ] + }, + { + "name": "CrashCarsTogetherSinglePass", + "path": "src/DETHRACE/common/car.c", + "lines": [ + 6105, + 6332 + ] + }, + { + "name": "DoCollide", + "path": "src/DETHRACE/common/car.c", + "lines": [ + 6913, + 7219 + ] + }, + { + "name": "AddFrictionCarToCar", + "path": "src/DETHRACE/common/car.c", + "lines": [ + 3239, + 3297 + ] + }, + { + "name": "ModifyCarsMass", + "path": "src/DETHRACE/common/car.c", + "lines": [ + 6889, + 6897 + ] + }, + { + "name": "BringCarToAGrindingHalt", + "path": "src/DETHRACE/common/car.c", + "lines": [ + 6336, + 6343 + ] + }, + { + "name": "CrashCarsTogetherSinglePass", + "path": "src/DETHRACE/common/car.c", + "lines": [ + 6105, + 6332 + ] + } + ], + "dependsOn": [ + "wall-collision" + ] + }, + { + "id": "crush-damage", + "name": "Crumple deformation", + "purpose": "Deforming the render mesh around an impact point within per-vertex limits, propagating the displacement to neighbouring vertices, and reshrinking the collision box to match the deformed body", + "paths": [ + "src/DETHRACE/common/crush.c", + "src/DETHRACE/common/car.c" + ], + "functions": [ + { + "name": "CrushModel", + "path": "src/DETHRACE/common/crush.c", + "lines": [ + 273, + 311 + ] + }, + { + "name": "CrushModelPoint", + "path": "src/DETHRACE/common/crush.c", + "lines": [ + 157, + 269 + ] + }, + { + "name": "CrushAndDamageCar", + "path": "src/DETHRACE/common/car.c", + "lines": [ + 3431, + 3538 + ] + }, + { + "name": "CrushBoundingBox", + "path": "src/DETHRACE/common/car.c", + "lines": [ + 3590, + 3650 + ] + }, + { + "name": "ExpandBoundingBox", + "path": "src/DETHRACE/common/car.c", + "lines": [ + 3542, + 3586 + ] + }, + { + "name": "SetModelForUpdate", + "path": "src/DETHRACE/common/crush.c", + "lines": [ + 324, + 335 + ] + } + ], + "dependsOn": [ + "car-car-collision" + ] + }, + { + "id": "vehicle-damage", + "name": "Damage, breakdown and repair", + "purpose": "Classifying where an impact landed, matching the car's own damage rules against that classification, applying the result to named subsystems, deciding when a car is wrecked, and stepping the mesh back toward its undamaged shape over time", + "paths": [ + "src/DETHRACE/common/crush.c", + "src/DETHRACE/common/car.c" + ], + "functions": [ + { + "name": "DamageSystems", + "path": "src/DETHRACE/common/crush.c", + "lines": [ + 706, + 904 + ] + }, + { + "name": "DoDamage", + "path": "src/DETHRACE/common/crush.c", + "lines": [ + 615, + 626 + ] + }, + { + "name": "DamageUnit2", + "path": "src/DETHRACE/common/crush.c", + "lines": [ + 567, + 593 + ] + }, + { + "name": "SetKnackeredFlag", + "path": "src/DETHRACE/common/crush.c", + "lines": [ + 541, + 563 + ] + }, + { + "name": "RepairModel", + "path": "src/DETHRACE/common/crush.c", + "lines": [ + 365, + 409 + ] + }, + { + "name": "RepairCar2", + "path": "src/DETHRACE/common/crush.c", + "lines": [ + 413, + 434 + ] + }, + { + "name": "DoWheelDamage", + "path": "src/DETHRACE/common/crush.c", + "lines": [ + 1303, + 1361 + ] + }, + { + "name": "DoPratcamHit", + "path": "src/DETHRACE/common/crush.c", + "lines": [ + 676, + 702 + ] + } + ], + "dependsOn": [ + "crush-damage" + ] + }, + { + "id": "opponent-ai", + "name": "Opponent drivers", + "purpose": "The per-frame opponent tick that scores objectives, grows and decays a grudge against the player, decides whether to chase or race, and drives the pursuit, flee and recovery behaviour of each AI car", + "paths": [ + "src/DETHRACE/common/opponent.c" + ], + "functions": [ + { + "name": "MungeOpponents", + "path": "src/DETHRACE/common/opponent.c", + "lines": [ + 2506, + 2584 + ] + }, + { + "name": "NewObjective", + "path": "src/DETHRACE/common/opponent.c", + "lines": [ + 501, + 532 + ] + }, + { + "name": "ChooseNewObjective", + "path": "src/DETHRACE/common/opponent.c", + "lines": [ + 1613, + 1801 + ] + }, + { + "name": "ProcessThisOpponent", + "path": "src/DETHRACE/common/opponent.c", + "lines": [ + 1805, + 1829 + ] + }, + { + "name": "ProcessPursueAndTwat", + "path": "src/DETHRACE/common/opponent.c", + "lines": [ + 1054, + 1240 + ] + }, + { + "name": "ProcessRunAway", + "path": "src/DETHRACE/common/opponent.c", + "lines": [ + 1244, + 1299 + ] + }, + { + "name": "RematerialiseOpponent", + "path": "src/DETHRACE/common/opponent.c", + "lines": [ + 2116, + 2244 + ] + } + ], + "dependsOn": [] + }, + { + "id": "opponent-path", + "parent": "opponent-ai", + "name": "Opponent path following", + "purpose": "The route-following controller that turns path sections into throttle, brake and curvature commands, including the corner-speed lookahead and the damage-degraded thrust limits", + "paths": [ + "src/DETHRACE/common/oppoproc.c" + ], + "functions": [ + { + "name": "ProcessFollowPath", + "path": "src/DETHRACE/common/oppoproc.c", + "lines": [ + 132, + 487 + ] + }, + { + "name": "FollowCheatyPath", + "path": "src/DETHRACE/common/oppoproc.c", + "lines": [ + 491, + 618 + ] + }, + { + "name": "MaxCurvatureForCarSpeed", + "path": "src/DETHRACE/common/oppoproc.c", + "lines": [ + 89, + 98 + ] + }, + { + "name": "CornerFudge", + "path": "src/DETHRACE/common/oppoproc.c", + "lines": [ + 82, + 85 + ] + }, + { + "name": "GetStraight", + "path": "src/DETHRACE/common/oppoproc.c", + "lines": [ + 123, + 128 + ] + }, + { + "name": "StraightestArcForCorner2D", + "path": "src/DETHRACE/common/oppoproc.c", + "lines": [ + 12, + 24 + ] + } + ], + "dependsOn": [ + "opponent-ai" + ] + }, + { + "id": "chase-camera", + "name": "External chase camera", + "purpose": "Positioning, swinging, aiming and collision-avoiding the third-person camera that follows the car being driven", + "paths": [ + "src/DETHRACE/common/car.c" + ], + "functions": [ + { + "name": "NormalPositionExternalCamera", + "path": "src/DETHRACE/common/car.c", + "lines": [ + 5383, + 5502 + ] + }, + { + "name": "MoveWithWheels", + "path": "src/DETHRACE/common/car.c", + "lines": [ + 5506, + 5536 + ] + }, + { + "name": "SwingCamera", + "path": "src/DETHRACE/common/car.c", + "lines": [ + 5540, + 5654 + ] + }, + { + "name": "PointCameraAtCar", + "path": "src/DETHRACE/common/car.c", + "lines": [ + 5658, + 5699 + ] + }, + { + "name": "CollideCamera2", + "path": "src/DETHRACE/common/car.c", + "lines": [ + 5729, + 5854 + ] + }, + { + "name": "CollideCameraWithOtherCars", + "path": "src/DETHRACE/common/car.c", + "lines": [ + 5871, + 5904 + ] + } + ], + "dependsOn": [] + }, + { + "id": "incident-camera", + "parent": "chase-camera", + "name": "Incident and panning cameras", + "purpose": "The automatic cut to a nearby crash or pedestrian strike, the framing rules that decide where that camera stands, and the slow panning fallback used when there is nothing worth cutting to", + "paths": [ + "src/DETHRACE/common/car.c" + ], + "functions": [ + { + "name": "IncidentCam", + "path": "src/DETHRACE/common/car.c", + "lines": [ + 5013, + 5152 + ] + }, + { + "name": "MoveCamToIncident", + "path": "src/DETHRACE/common/car.c", + "lines": [ + 5156, + 5231 + ] + }, + { + "name": "PanningExternalCamera", + "path": "src/DETHRACE/common/car.c", + "lines": [ + 5235, + 5256 + ] + }, + { + "name": "SetUpPanningCamera", + "path": "src/DETHRACE/common/car.c", + "lines": [ + 5273, + 5355 + ] + }, + { + "name": "PointCamera", + "path": "src/DETHRACE/common/car.c", + "lines": [ + 5703, + 5725 + ] + } + ], + "dependsOn": [ + "chase-camera" + ] + } + ], + "cards": [ + { + "id": "fixed-step-simulation", + "node": "car-physics", + "name": "Fixed mechanics tick", + "summary": "Forty-millisecond mechanics ticks with a bounded catch-up, a single scale between the physics space and the world, and drag that depends on volume", + "behavior": "The simulation does not advance at the display rate. Mechanics move in fixed ticks of forty milliseconds, and one rendered frame may consume at most five of them, so a slow frame is caught up as several separate advances rather than one long one. A frame that arrives before the next tick boundary only interpolates between the previous and next car transforms, which is what keeps motion smooth when the frame rate and the tick rate disagree. Positions and velocities live in a finer internal space and are divided by a single factor of 6.9 whenever they cross into or out of the mechanics pass, and that one factor is what makes the rest of the model's numbers readable. Gravity is a constant ten in that space, applied along whichever body axis currently points down, so a car on a steep bank feels gravity through its own frame rather than the world's. Every tick also bleeds velocity and spin by a drag term proportional to speed, and a car that can operate underwater keeps only sixty percent of the drag it would otherwise suffer inside a viscous volume.", + "constants": [ + { + "name": "tickMilliseconds", + "value": 40, + "unit": "milliseconds of simulated time per mechanics tick", + "source": "src/DETHRACE/constants.h:912" + }, + { + "name": "catchUpTicksPerFrame", + "value": 5, + "unit": "mechanics ticks per rendered frame", + "source": "src/DETHRACE/common/car.c:235" + }, + { + "name": "gravity", + "value": 10, + "unit": "world units per second squared per tick", + "source": "src/DETHRACE/constants.h:914" + }, + { + "name": "engineSpaceScale", + "value": 6.9, + "unit": "engine units per world unit applied at the mechanics boundary", + "source": "src/DETHRACE/constants.h:905" + }, + { + "name": "dragCoefficient", + "value": 0.0005, + "unit": "inverse milliseconds per tick", + "source": "src/DETHRACE/constants.h:901" + }, + { + "name": "underwaterDragShare", + "value": 0.6, + "unit": "share of the water gravity reduction kept by a car that can drive underwater, per tick", + "source": "src/DETHRACE/common/car.c:2312" + } + ], + "sources": [ + { + "path": "src/DETHRACE/common/car.c", + "line": 1293 + }, + { + "path": "src/DETHRACE/common/car.c", + "line": 1344 + }, + { + "path": "src/DETHRACE/common/car.c", + "line": 2138 + }, + { + "path": "src/DETHRACE/constants.h", + "line": 912 + } + ], + "capabilities": [ + "fixed timestep", + "frame interpolation", + "body-axis gravity", + "speed-squared drag" + ], + "tags": [ + "physics", + "timestep", + "integration", + "feel" + ] + }, + { + "id": "angular-integration", + "node": "car-physics", + "name": "Spin integration and substeps", + "summary": "Large angular velocities are broken into short rotational substeps and tiny ones are skipped entirely", + "behavior": "Rotating the body is handled separately from moving it. Each tick the squared magnitude of the body's spin is compared against two thresholds. Below the lower one the body does not rotate at all, so a car at rest cannot creep. Above the higher one the tick is cut into a number of equal pieces roughly proportional to the square root of the excess spin, so a violent impact that would otherwise wrap the transform visibly is resolved as several small rotations, capped at twenty pieces. Under the threshold a single first-order step is used; over it the integrator rotates by half the angle, re-derives the spin from the inverse inertia tensor, and then rotates by the remaining full angle, which stops an airborne wreck from gaining energy it never had. After the tick, any body whose spin on any axis passes ten thousand in either direction has its velocity and its spin zeroed outright, a guard against a numeric blow-up turning into a car leaving the map.", + "constants": [ + { + "name": "negligibleSpin", + "value": ".0000001", + "unit": "squared radians scaled by the tick", + "source": "src/DETHRACE/common/car.c:2027" + }, + { + "name": "substepSpinTrigger", + "value": ".008", + "unit": "squared radians scaled by the tick", + "source": "src/DETHRACE/common/car.c:2031" + }, + { + "name": "substepSpinDivisor", + "value": ".032", + "unit": "squared radians per substep", + "source": "src/DETHRACE/common/car.c:2032" + }, + { + "name": "substepCap", + "value": 20, + "unit": "substeps per update", + "source": "src/DETHRACE/common/car.c:2035" + }, + { + "name": "runawaySpinLimit", + "value": 10000, + "unit": "radians per second per update", + "source": "src/DETHRACE/common/car.c:1107" + } + ], + "sources": [ + { + "path": "src/DETHRACE/common/car.c", + "line": 2019 + }, + { + "path": "src/DETHRACE/common/car.c", + "line": 1935 + }, + { + "path": "src/DETHRACE/common/car.c", + "line": 2024 + }, + { + "path": "src/DETHRACE/common/car.c", + "line": 1099 + } + ], + "capabilities": [ + "tumbling", + "rotation substepping", + "numeric guard" + ], + "tags": [ + "physics", + "rotation", + "tumbling", + "stability" + ] + }, + { + "id": "suspension-contact", + "node": "suspension", + "name": "Wheel contact and suspension travel", + "summary": "Each wheel rays to the ground and pushes back with a spring-damper pair that levers pitch and roll through its offset from the centre of mass", + "behavior": "All four wheel positions are transformed into the world and each is cast against the floor surface. A wheel counts as touching when the measured distance falls between a small allowance for penetration and that axle's full suspension travel, widened by the wheel's own damage offset. A wheel outside that band contributes nothing and its stored compression is reset to full extension. While touching, the wheel pushes along the surface normal with a stiffness term proportional to how far it is compressed from rest, minus a damping term proportional to how quickly that compression is changing over the tick, and the damping never pulls the car down. The push is summed into the vertical force and simultaneously converted into pitch and roll torque through the wheel's lever arm from the centre of mass, which is why a car landing on one front corner rotates instead of sinking flat. Standing on all four wheels no single wheel may carry more than a quarter of twice the body weight, and the stiffness and damping themselves are derived at setup time from the body mass split by the front-to-rear lever ratio, divided by the tuning suspension give, with the damper using the square root of the same factor so a soft spring is not paired with a hard damper.", + "constants": [ + { + "name": "contactAllowance", + "value": -0.5, + "unit": "world units of penetration still counted as contact per tick", + "source": "src/DETHRACE/common/car.c:2284" + }, + { + "name": "restingLoadWeightShare", + "value": 2, + "unit": "multiples of body weight allowed at a resting wheel", + "source": "src/DETHRACE/common/car.c:2291" + }, + { + "name": "restingLoadDivisor", + "value": 4, + "unit": "divisor across the four wheels", + "source": "src/DETHRACE/common/car.c:2291" + }, + { + "name": "stiffnessMassFactor", + "value": 5, + "unit": "stiffness multipliers applied to body mass per unit of suspension give", + "source": "src/DETHRACE/common/car.c:7365" + }, + { + "name": "stiffnessBiasOffset", + "value": 1, + "unit": "unit added to the axle mass split before stiffness is divided", + "source": "src/DETHRACE/common/car.c:7373" + } + ], + "sources": [ + { + "path": "src/DETHRACE/common/car.c", + "line": 2284 + }, + { + "path": "src/DETHRACE/common/car.c", + "line": 2288 + }, + { + "path": "src/DETHRACE/common/car.c", + "line": 7358 + }, + { + "path": "src/DETHRACE/common/car.c", + "line": 7373 + } + ], + "capabilities": [ + "raycast wheels", + "spring damper", + "load transfer", + "damaged ride height" + ], + "tags": [ + "physics", + "suspension", + "wheels", + "contact" + ] + }, + { + "id": "surface-bumpiness", + "node": "suspension", + "name": "Procedural surface bumpiness", + "summary": "A hashed lattice turns a material's bumpiness value into an extra displacement at each wheel contact", + "behavior": "Rough ground is not modelled as geometry. Each contacting wheel's world position is multiplied by 512 and folded modulo 2048 on the two horizontal axes, producing a pair of triangular-wave coordinates inside half that period. The folded values are summed and remapped so the result peaks once per period and falls away in a diamond around the peak, then a constant of 400 is subtracted and the remainder is floored at zero, which opens a flat plateau between peaks. What survives is multiplied by the material's own bumpiness value and divided by 42400 before being added to the contact distance along the surface normal. The effect is a repeatable but irregular shake at a scale far below the collision mesh, and because the four wheels sample different phases of the same lattice the body pitches and rolls rather than bouncing evenly. A material with zero bumpiness is perfectly flat, so the same suspension reads as glassy on tarmac and loose on gravel without any change to the car itself.", + "constants": [ + { + "name": "sampleScale", + "value": 512, + "unit": "samples per world unit", + "source": "src/DETHRACE/common/car.c:2169" + }, + { + "name": "latticePeriod", + "value": 2048, + "unit": "samples per lattice repeat", + "source": "src/DETHRACE/common/car.c:2169" + }, + { + "name": "foldHalfPeriod", + "value": 1024, + "unit": "samples in half a lattice period", + "source": "src/DETHRACE/common/car.c:2172" + }, + { + "name": "plateauOffset", + "value": 400, + "unit": "samples subtracted before displacement is applied", + "source": "src/DETHRACE/common/car.c:2183" + }, + { + "name": "heightNormaliser", + "value": 42400, + "unit": "samples per unit of surface height", + "source": "src/DETHRACE/common/car.c:2187" + } + ], + "sources": [ + { + "path": "src/DETHRACE/common/car.c", + "line": 2158 + }, + { + "path": "src/DETHRACE/common/car.c", + "line": 2169 + }, + { + "path": "src/DETHRACE/common/car.c", + "line": 2187 + }, + { + "path": "src/DETHRACE/common/car.c", + "line": 2281 + } + ], + "capabilities": [ + "procedural roughness", + "material driven", + "contact offset" + ], + "tags": [ + "physics", + "surfaces", + "feel", + "off-road" + ] + }, + { + "id": "tyre-grip-limit", + "node": "tyre-model", + "name": "Tyre grip and the friction ellipse", + "summary": "Grip grows with tyre load and is shared between cornering and driving, so a tyre that is turning has nothing left to accelerate with", + "behavior": "A wheel's available friction is the local coefficient multiplied by the square root of the load pressing it down, so a wheel lifting off loses grip quickly and a loaded one gains it. The coefficient blends a per-car grip multiplier, an oil contamination factor and the surface's own tyre friction, and where the car is sliding sideways the blend is pushed toward a higher sliding value by a fraction of the lateral wheel speed divided by ten, capped at one, so a sliding tyre never becomes frictionless. The lateral and longitudinal demands are then combined into a single vector whose length is half their sum, and the per-axle budget is applied to that length; surplus is simply lost, which is what makes a car understeer out of a corner instead of cornering and accelerating at once. Rear and front are budgeted and clamped independently. A wheel that exceeds its budget is flagged as slipping, which drives the skid marks, the skid audio and the loss of steering authority, and traction control trims the engine impulse to a hundredth over the exact limit rather than cutting it, with a floor of four tenths of the available grip kept while the engine speed is above target.", + "constants": [ + { + "name": "lateralGripBlendDivisor", + "value": 10, + "unit": "world units per second of lateral speed per unit of blend", + "source": "src/DETHRACE/common/car.c:2436" + }, + { + "name": "lateralGripBlendCap", + "value": 1, + "unit": "fraction of the blend applied", + "source": "src/DETHRACE/common/car.c:2437" + }, + { + "name": "frictionVectorDivisor", + "value": 2, + "unit": "divisor when combining the two force components", + "source": "src/DETHRACE/common/car.c:2423" + }, + { + "name": "forceFloor", + "value": 0.0001, + "unit": "normalised force fraction below which no rescale is applied", + "source": "src/DETHRACE/common/car.c:2489" + }, + { + "name": "skidSlipPlayer", + "value": 20, + "unit": "normalised slip units for the car being driven", + "source": "src/DETHRACE/constants.h:921" + }, + { + "name": "skidSlipOther", + "value": 60, + "unit": "normalised slip units for every other car", + "source": "src/DETHRACE/constants.h:922" + }, + { + "name": "tractionControlOvershoot", + "value": 1.01, + "unit": "multiplier on the corrected engine impulse", + "source": "src/DETHRACE/common/car.c:2450" + }, + { + "name": "gripFloorAboveTarget", + "value": 0.4, + "unit": "fraction of grip retained while engine speed exceeds target", + "source": "src/DETHRACE/common/car.c:2464" + } + ], + "sources": [ + { + "path": "src/DETHRACE/common/car.c", + "line": 2442 + }, + { + "path": "src/DETHRACE/common/car.c", + "line": 2496 + }, + { + "path": "src/DETHRACE/common/car.c", + "line": 2506 + }, + { + "path": "src/DETHRACE/constants.h", + "line": 921 + } + ], + "capabilities": [ + "load sensitive grip", + "combined slip", + "wheel slip flags", + "skid feedback" + ], + "tags": [ + "physics", + "tyres", + "grip", + "handling" + ] + }, + { + "id": "engine-torque", + "node": "drivetrain", + "name": "Engine torque and throttle", + "summary": "Torque starts negative from engine braking, gains a throttle demand, and is cut down as the engine takes damage", + "behavior": "The torque at any instant begins as a loss, a term that grows with the square of engine speed divided by a hundred million minus a small constant, so a car coasts down when the throttle is released. Holding the throttle adds a demand of 1.2 for a digital input; on an analogue axis the demand is the raw value divided by 54613. When the car is in a low gear, turning hard and traction control is already intervening, the demand is trimmed to 0.7 so the rear tyres do not light up mid-corner. The demand is multiplied by a per-car power factor and by whichever power-up level the driver has collected, then reduced by one percent per damage level once the engine has taken more than ten levels. The result is divided by the current gear number before it becomes an impulse at the wheels, so a tall gear accelerates gently. Where engine speed is chasing the speed the car is really doing, an extra term corrects toward the target against a fixed two-hundred-microsecond inertia, which is what makes the response feel elastic instead of instant.", + "constants": [ + { + "name": "engineBrakeDivisor", + "value": 100000000, + "unit": "engine speed squared per unit of torque", + "source": "src/DETHRACE/common/car.c:1004" + }, + { + "name": "engineBrakeBaseline", + "value": 0.2, + "unit": "torque units", + "source": "src/DETHRACE/common/car.c:1004" + }, + { + "name": "tractionTrimThrottle", + "value": 0.7, + "unit": "throttle units", + "source": "src/DETHRACE/common/car.c:1007" + }, + { + "name": "analogueThrottleDivisor", + "value": 54613, + "unit": "raw axis units per unit of throttle", + "source": "src/DETHRACE/common/car.c:1009" + }, + { + "name": "digitalThrottle", + "value": 1.2, + "unit": "throttle units", + "source": "src/DETHRACE/common/car.c:1011" + }, + { + "name": "damageFreeEngineLevel", + "value": 10, + "unit": "damage levels before power begins to fall", + "source": "src/DETHRACE/common/car.c:1015" + }, + { + "name": "damagePowerDivisor", + "value": 100, + "unit": "damage levels per unit of lost power", + "source": "src/DETHRACE/common/car.c:1016" + }, + { + "name": "revInertiaDivisor", + "value": 0.0002, + "unit": "seconds per unit of engine inertia", + "source": "src/DETHRACE/common/car.c:1050" + } + ], + "sources": [ + { + "path": "src/DETHRACE/common/car.c", + "line": 971 + }, + { + "path": "src/DETHRACE/common/car.c", + "line": 1004 + }, + { + "path": "src/DETHRACE/common/car.c", + "line": 1041 + }, + { + "path": "src/DETHRACE/common/car.c", + "line": 1050 + } + ], + "capabilities": [ + "torque curve", + "throttle response", + "damage limited power", + "analogue input" + ], + "tags": [ + "drivetrain", + "engine", + "acceleration", + "tuning" + ] + }, + { + "id": "gearbox-shifting", + "node": "drivetrain", + "name": "Automatic gearbox", + "summary": "Gear selection follows engine speed with hysteresis, and reverse is flipped into rather than shifted into", + "behavior": "The gearbox is automatic and has no clutch input. Engine speed is integrated from the load at the wheels with a fixed inertia step, and the speed the engine would ideally hold is the car's forward speed divided by a per-car speed-to-revs ratio and by the gear number, so the same engine speed means a higher road speed in a taller gear. A gear engages only once the free-revving engine passes a thousand with the throttle held; at that moment first or reverse is chosen depending on which way the driver asked to go. An upshift needs both the target and the actual engine speed above six thousand and the rear wheels not slipping, which stops a wheelspin from being read as acceleration. A downshift happens when the target drops below three thousand. Sliding back to neutral needs both speeds under two hundred with no throttle, so a car settles to idle on its own. Braking while in first or second and still rolling slower than one unit flips the sign of the gear, which is how reverse is reached without a dedicated control. Traction control pins engine speed to the target whenever it would overshoot, so the tachometer cannot run away while the tyres are scrabbling, and a fresh change is held locked for a moment until the speeds match again.", + "constants": [ + { + "name": "shiftUpSpeed", + "value": 6000, + "unit": "engine speed units", + "source": "src/DETHRACE/common/car.c:2746" + }, + { + "name": "shiftDownSpeed", + "value": 3000, + "unit": "engine speed units", + "source": "src/DETHRACE/common/car.c:2749" + }, + { + "name": "driveEngageSpeed", + "value": 1000, + "unit": "engine speed units", + "source": "src/DETHRACE/common/car.c:2722" + }, + { + "name": "neutralSpeed", + "value": 200, + "unit": "engine speed units", + "source": "src/DETHRACE/common/car.c:2752" + }, + { + "name": "reverseGearCeiling", + "value": 2, + "unit": "gear index up to which a brake press flips direction", + "source": "src/DETHRACE/common/car.c:2731" + }, + { + "name": "revInertiaStep", + "value": 0.0002, + "unit": "seconds per unit of engine inertia", + "source": "src/DETHRACE/common/car.c:2735" + } + ], + "sources": [ + { + "path": "src/DETHRACE/common/car.c", + "line": 2699 + }, + { + "path": "src/DETHRACE/common/car.c", + "line": 2735 + }, + { + "path": "src/DETHRACE/common/car.c", + "line": 2746 + }, + { + "path": "src/DETHRACE/common/car.c", + "line": 2749 + } + ], + "capabilities": [ + "automatic gears", + "shift hysteresis", + "reverse engagement", + "traction control" + ], + "tags": [ + "drivetrain", + "gearbox", + "tuning", + "feel" + ] + }, + { + "id": "brake-and-handbrake", + "node": "drivetrain", + "name": "Brake ramp, axle split and handbrake", + "summary": "Brake force ramps to a cap, is divided between axles by their grip, and is eaten away by brake damage", + "behavior": "Braking is not an instant force. Pressing the pedal jumps the force to the car's per-car initial value and then climbs at a second per-car rate until it reaches their sum; releasing it drops back to nothing at once, so a tap gives a short pulse and a hold gives full stopping power. The total is then split between rear and front axles in proportion to their grip coefficients weighted by the friction ellipse, so a car with more grip at the back brakes harder at the back and stays straighter under heavy braking. Brake damage is averaged per axle and, past one fifth of the damage scale, reduces that axle's share with a squared falloff that reaches nothing at full damage, so a car with ruined rear brakes can barely stop. The handbrake takes the whole remaining retardation, applies it to the rear alone, and knocks the car into neutral so the throttle cannot fight it. It stops working altogether once either rear brake unit has taken sixty levels of damage.", + "constants": [ + { + "name": "brakeDamageGraceDivisor", + "value": 5, + "unit": "fifths of the damage scale before braking degrades", + "source": "src/DETHRACE/common/car.c:2394" + }, + { + "name": "brakeDamageSpan", + "value": 4, + "unit": "quarters of the damage scale over which braking fades to nothing", + "source": "src/DETHRACE/common/car.c:2395" + }, + { + "name": "handbrakeFailureDamage", + "value": 60, + "unit": "damage levels at which the handbrake stops working", + "source": "src/DETHRACE/constants.h:920" + }, + { + "name": "handbrakeNeutralGear", + "value": 0, + "unit": "gear index selected by the handbrake", + "source": "src/DETHRACE/common/car.c:2420" + } + ], + "sources": [ + { + "path": "src/DETHRACE/common/car.c", + "line": 971 + }, + { + "path": "src/DETHRACE/common/car.c", + "line": 2394 + }, + { + "path": "src/DETHRACE/common/car.c", + "line": 2417 + }, + { + "path": "src/DETHRACE/constants.h", + "line": 920 + } + ], + "capabilities": [ + "brake ramp", + "axle brake split", + "handbrake", + "brake damage" + ], + "tags": [ + "drivetrain", + "braking", + "damage", + "feel" + ] + }, + { + "id": "steering-curvature", + "node": "steering", + "name": "Steering as accumulated curvature", + "summary": "Input builds a path curvature toward a per-car limit, and that limit is reached more slowly the faster the car is going", + "behavior": "The driver does not command a steering angle, they command a curvature: how tightly the driven path bends. Holding a direction moves curvature toward the per-car maximum at one of two rates. When the input opposes the direction the car is already turning, the correction is a flat 0.01 normalised by the tick; when it agrees, or the car is running straight, the rate is proportional to 0.05 divided by five plus current speed, so at a crawl the wheels go lock to lock quickly and at racing speed the same key press takes noticeably longer. Two of the alternative control schemes reshape this: one triples the counter-steering correction while keeping three quarters of the entry rate, and another drives a steering velocity that is integrated into curvature and zeroed whenever the driver reverses direction, which damps the wheel without the counter-steer boost. The analogue stick is read as a squared curve normalised by a little over four billion raw units, so the first third of travel hardly turns the car and the last third does almost everything. Everything is finally clamped to the per-car maximum, so no scheme can exceed the car's own handling limit.", + "constants": [ + { + "name": "counterSteerRate", + "value": 0.01, + "unit": "curvature units per second of flat correction", + "source": "src/DETHRACE/common/car.c:1878" + }, + { + "name": "tickNormaliser", + "value": 0.04, + "unit": "seconds used to normalise a correction to one tick", + "source": "src/DETHRACE/common/car.c:1878" + }, + { + "name": "speedSteerNumerator", + "value": 0.05, + "unit": "curvature units per second", + "source": "src/DETHRACE/common/car.c:1880" + }, + { + "name": "speedSteerOffset", + "value": 5, + "unit": "speed units added to the rate divisor", + "source": "src/DETHRACE/common/car.c:1880" + }, + { + "name": "counterSteerScale", + "value": 3, + "unit": "multiplier on the flat correction when steering against the turn", + "source": "src/DETHRACE/common/car.c:1728" + }, + { + "name": "entryRateScale", + "value": 0.75, + "unit": "multiplier on the speed dependent entry rate", + "source": "src/DETHRACE/common/car.c:1730" + }, + { + "name": "stickCurveDivisor", + "value": 4294967300, + "unit": "raw axis units squared per curvature unit", + "source": "src/DETHRACE/common/car.c:1798" + } + ], + "sources": [ + { + "path": "src/DETHRACE/common/car.c", + "line": 1867 + }, + { + "path": "src/DETHRACE/common/car.c", + "line": 1721 + }, + { + "path": "src/DETHRACE/common/car.c", + "line": 1758 + }, + { + "path": "src/DETHRACE/common/car.c", + "line": 1869 + } + ], + "capabilities": [ + "curvature steering", + "speed sensitive rate", + "analogue curve", + "handling limit" + ], + "tags": [ + "steering", + "handling", + "input", + "feel" + ] + }, + { + "id": "steering-self-centre", + "node": "steering", + "name": "Steering self-centring", + "summary": "With no input the curvature bleeds away in step with the yaw the car is already carrying", + "behavior": "Releasing the steering does not snap the wheels straight. While a direction is held, or while the handbrake is down, self-centring is skipped entirely, and it only runs once the rear wheels have come off their rest height, which is the condition for the car being airborne or tipped. When it runs it compares two candidate corrections: one that would cancel the body's current spin about the surface normal, projected over the front-to-rear axle separation, and one that would cancel the whole remaining curvature. It applies whichever is smaller in magnitude, or the one of opposite sign, and then clamps the result so curvature cannot be pushed back across zero. The consequence is that a car sliding sideways keeps a self-aligning torque proportional to its yaw rate rather than to the driver's input, which is what makes a slide recoverable instead of snapping back, while a car in the air keeps whatever steering it left the ground with.", + "constants": [ + { + "name": "straightCurvature", + "value": 0, + "unit": "curvature units the alignment clamps back to", + "source": "src/DETHRACE/common/car.c:2073" + } + ], + "sources": [ + { + "path": "src/DETHRACE/common/car.c", + "line": 2048 + }, + { + "path": "src/DETHRACE/common/car.c", + "line": 2055 + }, + { + "path": "src/DETHRACE/common/car.c", + "line": 2076 + }, + { + "path": "src/DETHRACE/common/car.c", + "line": 2328 + } + ], + "capabilities": [ + "self-aligning torque", + "yaw rate feedback", + "airborne steering lock" + ], + "tags": [ + "steering", + "handling", + "feel" + ] + }, + { + "id": "wall-contact-solver", + "node": "wall-collision", + "name": "Wall contact response", + "summary": "Contact points are found by sweeping the car's box against nearby level faces and resolved with a simultaneous multi-point impulse solve", + "behavior": "Before a car moves, the level faces that could possibly touch it are gathered into a list capped at fifty, so the collision pass never has to consider the whole world. The car's box corners, plus a handful of extra points that stand proud of the box, are swept from their previous position to their current one and cast against those faces; the resulting hit distances become the contact points, with at most four kept and the most distant rejected. Each contact contributes a row to a small matrix built from the contact normal, the lever arm and the body's inverse inertia tensor, and the whole system is solved at once so that fixing one corner does not fight the next. The solved impulses are nudged up by a fraction of a percent so contact does not sit exactly on the boundary, and up to twenty re-collide attempts are made inside one tick before the car is simply stopped. Tangential friction takes thirty-five percent of the normal impulse, scaled by the material's car-wall friction, so a car grinds along a wall rather than sticking to it or sliding freely. If at least three points are in contact and the summed force points back through the wall with a vertical component inside a band between ninety and a hundred and ten units of acceleration, the car is launched: nearly-head-on cases pivot it about its own front axis at half a radian per second, and everything else is stopped dead.", + "constants": [ + { + "name": "faceListCap", + "value": 50, + "unit": "level faces considered per car per tick", + "source": "src/DETHRACE/common/car.c:2884" + }, + { + "name": "contactPointCap", + "value": 4, + "unit": "contact points kept per car per tick", + "source": "src/DETHRACE/common/car.c:2950" + }, + { + "name": "sweepInset", + "value": 0.0072463769, + "unit": "engine units of inset along the sweep direction", + "source": "src/DETHRACE/common/car.c:2926" + }, + { + "name": "reCollideAttempts", + "value": 20, + "unit": "collision passes per tick", + "source": "src/DETHRACE/common/car.c:1631" + }, + { + "name": "impulseScale", + "value": 1.001, + "unit": "multiplier on the solved impulse", + "source": "src/DETHRACE/common/car.c:3054" + }, + { + "name": "wallFrictionShare", + "value": 0.35, + "unit": "fraction of the normal impulse available as friction", + "source": "src/DETHRACE/common/car.c:3206" + }, + { + "name": "impactAnnounceForce", + "value": 10, + "unit": "squared force units before a loud impact is played", + "source": "src/DETHRACE/common/car.c:3068" + }, + { + "name": "launchBandMinimum", + "value": 90, + "unit": "acceleration units per second", + "source": "src/DETHRACE/common/car.c:3101" + }, + { + "name": "launchBandMaximum", + "value": 110, + "unit": "acceleration units per second", + "source": "src/DETHRACE/common/car.c:3102" + }, + { + "name": "launchSpinRate", + "value": 0.5, + "unit": "radians per second about the front axis", + "source": "src/DETHRACE/common/car.c:3118" + } + ], + "sources": [ + { + "path": "src/DETHRACE/common/car.c", + "line": 2787 + }, + { + "path": "src/DETHRACE/common/car.c", + "line": 2884 + }, + { + "path": "src/DETHRACE/common/car.c", + "line": 3054 + }, + { + "path": "src/DETHRACE/common/car.c", + "line": 3206 + } + ], + "capabilities": [ + "broadphase face list", + "swept box contacts", + "simultaneous impulse solve", + "wall friction", + "wall launch" + ], + "tags": [ + "physics", + "collision", + "world geometry", + "crash" + ] + }, + { + "id": "car-vs-car-solver", + "node": "car-car-collision", + "name": "Car to car collision", + "summary": "Pairs of contact points between two boxes are solved as one coupled impulse problem and rescanned in passes", + "behavior": "Two cars are tested first by bounding box and then by their real box geometry, producing matched pairs of contact points and normals, at most four pairs per collision. The solver restores both cars to where they began the tick, measures the closing speed at each pair, and builds one combined matrix from both bodies' inverse masses and inverse inertia tensors, so an impulse accounts for the spin it will induce as well as the slide. The small system is solved directly for one, two, three or four points, with degenerate cases falling back to fewer. Each solved impulse is padded to at least a thousandth and biased upward by a tenth so resting contact separates instead of jittering, and the total is applied as a velocity change only, never as a position fix, so overlap resolves over the following ticks. Tangential friction takes thirty-five percent of the normal impulse, limited by whatever would just stop the relative sliding, and is skipped when the two surfaces are already moving together. The whole set of pairs is rescanned up to five times per tick and if anything remains it is brought to a grinding halt. Each car is then handed the impulse it received for crush and damage processing, and a car flagged as immovable, such as a parked wreck, passes the energy straight back.", + "constants": [ + { + "name": "contactPairCap", + "value": 4, + "unit": "contact point pairs per collision", + "source": "src/DETHRACE/common/car.c:6959" + }, + { + "name": "solverPasses", + "value": 5, + "unit": "pair rescan passes per tick", + "source": "src/DETHRACE/common/car.c:6095" + }, + { + "name": "impulseFloor", + "value": 0.001, + "unit": "impulse units", + "source": "src/DETHRACE/common/car.c:7054" + }, + { + "name": "impulseBias", + "value": 0.1, + "unit": "impulse units added to every solved contact", + "source": "src/DETHRACE/common/car.c:7060" + }, + { + "name": "contactFrictionShare", + "value": 0.35, + "unit": "fraction of the normal impulse", + "source": "src/DETHRACE/common/car.c:3282" + }, + { + "name": "slidingStopFloor", + "value": 0.01, + "unit": "velocity units below which no friction is applied", + "source": "src/DETHRACE/common/car.c:3262" + }, + { + "name": "crushForceScale", + "value": 5, + "unit": "multiplier applied to the impulse before it becomes crush energy", + "source": "src/DETHRACE/common/car.c:7212" + }, + { + "name": "sparkSpeedShare", + "value": 0.25, + "unit": "fraction of the summed car velocities given to the spark shower", + "source": "src/DETHRACE/common/car.c:7215" + } + ], + "sources": [ + { + "path": "src/DETHRACE/common/car.c", + "line": 6079 + }, + { + "path": "src/DETHRACE/common/car.c", + "line": 6913 + }, + { + "path": "src/DETHRACE/common/car.c", + "line": 7054 + }, + { + "path": "src/DETHRACE/common/car.c", + "line": 7212 + } + ], + "capabilities": [ + "coupled impulse solve", + "car friction", + "multi-pass resolution", + "immovable bodies" + ], + "tags": [ + "physics", + "collision", + "car combat", + "feel" + ] + }, + { + "id": "crush-deformation", + "node": "crush-damage", + "name": "Crumple deformation", + "summary": "Impact energy drags the nearest body vertex and its neighbours inward within per-vertex limits", + "behavior": "Body damage is written into the render mesh rather than the collision box, and only for model parts near the view, so an opponent far from the camera is never deformed. The impulse is scaled by the part's own softness and the resulting length is compared with a small floor; below it nothing happens, above it the floor is subtracted so light knocks never accumulate. The vertex nearest the impact in model space takes the whole displacement along its stored soft direction, and each axis is clamped to limits recorded with that vertex; a displacement that would cross a limit is replaced by a small random offset sitting on the limit, so a crushed panel ends up ragged rather than flat-cut. Any component that would move the vertex back against the applied direction is discarded and restored, which is why a bent panel does not spring outward. Neighbouring vertices are then pulled along by a weight taken from their distance to the crush point and are further bent along a pseudorandom axis by a fold factor that grows with that weight and flips sign from a hash of the vertex position, which is where the uneven folded look comes from. The collision box is reshrunk around the deformed mesh so a crumpled nose is physically shorter, and if the reshrunk box cannot be placed clear of geometry the repair is refused and the box restored.", + "constants": [ + { + "name": "crushEnergyFloor", + "value": 0.06, + "unit": "softness scaled energy units per impact", + "source": "src/DETHRACE/common/crush.c:292" + }, + { + "name": "softnessReference", + "value": 0.7, + "unit": "softness units used as the reference divisor", + "source": "src/DETHRACE/common/crush.c:744" + }, + { + "name": "foldFactorHalfScale", + "value": 128, + "unit": "weight units at which the fold factor reverses", + "source": "src/DETHRACE/common/crush.c:184" + }, + { + "name": "neighbourDragScale", + "value": 256, + "unit": "weight units per unit of drag on a neighbouring vertex", + "source": "src/DETHRACE/common/crush.c:251" + }, + { + "name": "foldSignHashScale", + "value": 63, + "unit": "multiplier on the coordinate sum when choosing a fold sign", + "source": "src/DETHRACE/common/crush.c:187" + }, + { + "name": "bendAxisHashScale", + "value": 100, + "unit": "multiplier on the coordinate sum when choosing a bend axis", + "source": "src/DETHRACE/common/crush.c:253" + } + ], + "sources": [ + { + "path": "src/DETHRACE/common/crush.c", + "line": 273 + }, + { + "path": "src/DETHRACE/common/crush.c", + "line": 157 + }, + { + "path": "src/DETHRACE/common/crush.c", + "line": 292 + }, + { + "path": "src/DETHRACE/common/car.c", + "line": 3431 + } + ], + "capabilities": [ + "vertex crush", + "deformation limits", + "neighbour propagation", + "box reshrink" + ], + "tags": [ + "damage", + "deformation", + "crash", + "visuals" + ] + }, + { + "id": "impact-damage-routing", + "node": "vehicle-damage", + "name": "Impact classification and damage routing", + "summary": "The impact point is classified against the crushed bounding box and matched against the car's own damage rules", + "behavior": "An impact becomes damage through a rule set carried by each car rather than one health number. The impact point is measured against the crushed bounding box and the smallest of the three distances to the nearest pair of faces decides the region: if depth is smallest the hit is front or back, if the lateral distance is smallest it is left or right, and the two remaining proportions along the box become the coordinates the rules test. Each car carries clauses with conditions on those proportions and on the region, and every clause that matches fires its effects with the impact energy as the magnitude and a per-effect weakness factor. For the player the damage is first rolled as a percentage chance proportional to fifteen hundred times the scaled energy and then applied at thirty times that energy; across a network it lands unconditionally at fifteen times; for an opponent it is applied at ten times and scaled upward by the race's suggested rank. The same energy sets how hard the view kicks, with front and rear hits swinging furthest and left and right hits using a shorter, faster wobble.", + "constants": [ + { + "name": "networkDamageScale", + "value": 15, + "unit": "damage levels per unit of energy", + "source": "src/DETHRACE/common/crush.c:619" + }, + { + "name": "playerDamageChanceScale", + "value": 1500, + "unit": "percent per unit of energy", + "source": "src/DETHRACE/common/crush.c:620" + }, + { + "name": "playerDamageScale", + "value": 30, + "unit": "damage levels per unit of energy", + "source": "src/DETHRACE/common/crush.c:621" + }, + { + "name": "opponentDamageScale", + "value": 10, + "unit": "damage levels per unit of energy", + "source": "src/DETHRACE/common/crush.c:624" + }, + { + "name": "opponentRankFloor", + "value": 10, + "unit": "suggested rank below which the floor value is used", + "source": "src/DETHRACE/common/crush.c:624" + }, + { + "name": "opponentRankDivisor", + "value": 20, + "unit": "suggested rank per unit of extra damage", + "source": "src/DETHRACE/common/crush.c:624" + }, + { + "name": "shakeAmplitudeMinimum", + "value": 50, + "unit": "view units per unit of energy", + "source": "src/DETHRACE/common/crush.c:863" + }, + { + "name": "shakeAmplitudeMaximum", + "value": 100, + "unit": "view units per unit of energy", + "source": "src/DETHRACE/common/crush.c:863" + } + ], + "sources": [ + { + "path": "src/DETHRACE/common/crush.c", + "line": 706 + }, + { + "path": "src/DETHRACE/common/crush.c", + "line": 615 + }, + { + "path": "src/DETHRACE/common/crush.c", + "line": 863 + }, + { + "path": "src/DETHRACE/common/car.c", + "line": 3431 + } + ], + "capabilities": [ + "impact region detection", + "damage clauses", + "character dependent damage", + "hit feedback" + ], + "tags": [ + "damage", + "crash", + "feedback", + "tuning" + ] + }, + { + "id": "subsystem-damage", + "node": "vehicle-damage", + "name": "Subsystem damage, breakdown and repair", + "summary": "Damage lands on named parts in whole units, wrecking is threshold based, and repair crawls back vertex by vertex", + "behavior": "Damage is not a single bar. It is applied to named parts, covering engine, transmission, driver, four wheels and four brakes, and a hit worth less than five levels is ignored outright, which stops a long scrape from slowly destroying a car. Each part saturates at ninety-nine, and a hit smaller than ten levels cannot push a part across that line, so a car is only finished by a real impact. A car is declared wrecked when the engine, transmission or driver reaches ninety-nine, or when all four wheels do, and it then spills oil, stops smoking the way a running car does and falls silent on the radio. Wheel damage past thirty levels rotates that wheel's model out of true by a per-level wobble on two axes, mirrored onto the opposite-side wheel, and that rotation is measured back as a vertical sag the suspension then has to carry, so a damaged corner both looks and behaves broken — the wobble is skipped when the wheel is far outside the view. Repair runs the crush process backwards: every vertex of every undamaged-vertex record is stepped a tiny fraction toward the shape it once had each millisecond, so a car visibly straightens instead of snapping back. Impacts also drive a reaction in the cockpit view overlay, sorted into three strength bands by the squared length of the hit vector and aimed to the side the force came from.", + "constants": [ + { + "name": "minimumDamagePerHit", + "value": 5, + "unit": "damage levels per hit", + "source": "src/DETHRACE/common/crush.c:575" + }, + { + "name": "damageCeiling", + "value": 99, + "unit": "damage levels", + "source": "src/DETHRACE/common/crush.c:579" + }, + { + "name": "finishingHitMinimum", + "value": 10, + "unit": "damage levels required to finish a part", + "source": "src/DETHRACE/common/crush.c:580" + }, + { + "name": "wreckThreshold", + "value": 99, + "unit": "damage levels on a critical part", + "source": "src/DETHRACE/common/crush.c:545" + }, + { + "name": "wheelWobbleStart", + "value": 30, + "unit": "damage levels before a wheel goes out of true", + "source": "src/DETHRACE/common/crush.c:1323" + }, + { + "name": "wheelWobbleRange", + "value": 32, + "unit": "world units beyond which the wobble is skipped", + "source": "src/DETHRACE/common/crush.c:1324" + }, + { + "name": "repairStepPerMillisecond", + "value": 0.00005, + "unit": "mesh vertex units per millisecond", + "source": "src/DETHRACE/common/crush.c:427" + }, + { + "name": "strongHitBand", + "value": 0.2, + "unit": "squared hit magnitude for the strongest reaction", + "source": "src/DETHRACE/common/crush.c:682" + }, + { + "name": "mediumHitBand", + "value": 0.015, + "unit": "squared hit magnitude for the middle reaction", + "source": "src/DETHRACE/common/crush.c:684" + }, + { + "name": "negligibleHitBand", + "value": 0.001, + "unit": "squared hit magnitude below which nothing reacts", + "source": "src/DETHRACE/common/crush.c:686" + } + ], + "sources": [ + { + "path": "src/DETHRACE/common/crush.c", + "line": 567 + }, + { + "path": "src/DETHRACE/common/crush.c", + "line": 541 + }, + { + "path": "src/DETHRACE/common/crush.c", + "line": 427 + }, + { + "path": "src/DETHRACE/common/crush.c", + "line": 1303 + } + ], + "capabilities": [ + "part level damage", + "wreck thresholds", + "progressive repair", + "wheel damage" + ], + "tags": [ + "damage", + "state machine", + "repair", + "feedback" + ] + }, + { + "id": "ai-objectives", + "node": "opponent-ai", + "name": "Opponent objectives and grudges", + "summary": "Each opponent holds one timed objective and picks the next from a grudge that the player builds and time wears down", + "behavior": "Opponents are not one racing line with a throttle. Each holds exactly one objective at a time — finish the race, get near the player, pursue and ram the player, run away, return to a start point, wait for a victim, hover, or sit frozen — and each is given a lifespan of a random thirty to a hundred and eighty seconds, with a pursuit extended by another ninety because chasing takes longer than merely driving. Choosing the next objective is largely temperament. Each driver has a nastiness value that raises the base grudge it carries against the player by forty per point plus ten, and any time it rams or is rammed the grudge rises, clamped between a floor and a ceiling. When the player comes into view the chance of switching to pursuit is the grudge minus a standing offset plus thirty per point of nastiness, with a large bonus if the player is heading straight at it, and the grudge also decays on a slow timer so a grudge does not last forever. No more than three opponents will close on the player at once and no more than two will mind the race, which keeps the pressure varied instead of uniformly hostile. A pursuit ends once the grudge falls below a low threshold, and an opponent jammed against a stationary player for a second or two backs off with a burst of reverse thrust and then brakes, rather than grinding.", + "constants": [ + { + "name": "objectiveMinSeconds", + "value": 30, + "unit": "seconds", + "source": "src/DETHRACE/common/opponent.c:509" + }, + { + "name": "objectiveMaxSeconds", + "value": 180, + "unit": "seconds", + "source": "src/DETHRACE/common/opponent.c:509" + }, + { + "name": "objectiveTimeScale", + "value": 1000, + "unit": "milliseconds per second", + "source": "src/DETHRACE/common/opponent.c:509" + }, + { + "name": "pursuitExtension", + "value": 90000, + "unit": "milliseconds added to a pursuit objective", + "source": "src/DETHRACE/common/opponent.c:511" + }, + { + "name": "pursuitGrudgeFloor", + "value": 15, + "unit": "grudge units below which pursuit ends", + "source": "src/DETHRACE/common/opponent.c:1103" + }, + { + "name": "nastinessGrudgeScale", + "value": 40, + "unit": "grudge units per point of driver nastiness", + "source": "src/DETHRACE/common/opponent.c:1680" + }, + { + "name": "grudgeBaseline", + "value": 10, + "unit": "grudge units", + "source": "src/DETHRACE/common/opponent.c:1680" + }, + { + "name": "grudgeCeiling", + "value": 100, + "unit": "grudge units", + "source": "src/DETHRACE/common/opponent.c:1701" + }, + { + "name": "grudgeFloor", + "value": 20, + "unit": "grudge units", + "source": "src/DETHRACE/common/opponent.c:1701" + }, + { + "name": "headOnPursuitBonus", + "value": 50, + "unit": "percent added to the pursuit chance", + "source": "src/DETHRACE/common/opponent.c:1743" + }, + { + "name": "concurrentPursuers", + "value": 3, + "unit": "opponents allowed to close on the player", + "source": "src/DETHRACE/common/opponent.c:1764" + }, + { + "name": "concurrentRacers", + "value": 2, + "unit": "opponents allowed to mind the race", + "source": "src/DETHRACE/common/opponent.c:1769" + }, + { + "name": "backupDelay", + "value": 2200, + "unit": "milliseconds before an opponent backs off", + "source": "src/DETHRACE/common/opponent.c:1163" + }, + { + "name": "backupBrakeForce", + "value": 15, + "unit": "body masses per second squared", + "source": "src/DETHRACE/common/opponent.c:1165" + }, + { + "name": "reverseThrust", + "value": -8, + "unit": "body masses per second squared", + "source": "src/DETHRACE/common/opponent.c:1174" + } + ], + "sources": [ + { + "path": "src/DETHRACE/common/opponent.c", + "line": 2506 + }, + { + "path": "src/DETHRACE/common/opponent.c", + "line": 501 + }, + { + "path": "src/DETHRACE/common/opponent.c", + "line": 1613 + }, + { + "path": "src/DETHRACE/common/opponent.c", + "line": 1054 + } + ], + "capabilities": [ + "objective state machine", + "grudge model", + "aggression balancing", + "recovery behaviour" + ], + "tags": [ + "ai", + "opponents", + "combat", + "tuning" + ] + }, + { + "id": "ai-path-following", + "node": "opponent-path", + "name": "Opponent path following", + "summary": "A proportional-derivative speed controller chases a per-section target speed, with corner speeds taken from the geometry ahead", + "behavior": "An opponent drives by writing the same throttle, brake and curvature values a player's input would produce, from a controller that works on the track's path sections. It projects its position onto the current section's axis, looks a fixed fifteen world units ahead, and decides steering from the sideways error: outside the section's half-width on either side it takes the full available curvature and drops its target speed to six; just inside it nibbles a twentieth of the available curvature while asking for the open-road value; inside it steers straight at the open-road value. Available curvature shrinks inversely with speed above twelve and a half, so the same corner demands more wheel at speed. Ahead of the car it walks up to four further sections, fits a circular arc through each corner, and derives a corner speed from the arc radius times ten and a constant grip fudge of 1.4, clamped by the section's own maximum; whenever the distance to that corner is under speed squared over twenty-four plus one and a half times speed, the target drops to the corner speed. A proportional-derivative loop then turns the error into an acceleration, with a derivative term scaled by the measured frame time and a ceiling of ten body masses per second squared, degraded by engine and transmission damage and cut to nothing once either reaches the dead threshold. A negative demand is written as brake rather than negative throttle, so the opponent slows the way a player does.", + "constants": [ + { + "name": "cornerGripFudge", + "value": 1.4, + "unit": "grip multiplier applied to the corner radius", + "source": "src/DETHRACE/common/oppoproc.c:84" + }, + { + "name": "cornerSpeedAccel", + "value": 10, + "unit": "world units per second squared per world unit of radius", + "source": "src/DETHRACE/common/oppoproc.c:422" + }, + { + "name": "brakingDivisor", + "value": 24, + "unit": "world units per second squared per world unit of distance", + "source": "src/DETHRACE/common/oppoproc.c:405" + }, + { + "name": "brakingLead", + "value": 1.5, + "unit": "seconds of reaction distance", + "source": "src/DETHRACE/common/oppoproc.c:405" + }, + { + "name": "lowSpeedSteerCutoff", + "value": 12.5, + "unit": "world units per second", + "source": "src/DETHRACE/common/oppoproc.c:92" + }, + { + "name": "lookAhead", + "value": 15, + "unit": "world units along the section", + "source": "src/DETHRACE/common/oppoproc.c:315" + }, + { + "name": "openRoadSpeed", + "value": 80, + "unit": "world units per second", + "source": "src/DETHRACE/common/oppoproc.c:385" + }, + { + "name": "slowCornerSpeed", + "value": 6, + "unit": "world units per second", + "source": "src/DETHRACE/common/oppoproc.c:379" + }, + { + "name": "offPathSteerShare", + "value": 0.05, + "unit": "fraction of available curvature", + "source": "src/DETHRACE/common/oppoproc.c:384" + }, + { + "name": "maxThrust", + "value": 10, + "unit": "body masses per second squared", + "source": "src/DETHRACE/common/oppoproc.c:460" + }, + { + "name": "derivativeGain", + "value": 0.1, + "unit": "per unit of speed error per second", + "source": "src/DETHRACE/common/oppoproc.c:464" + }, + { + "name": "derivativeTimeScale", + "value": 1000, + "unit": "milliseconds per second", + "source": "src/DETHRACE/common/oppoproc.c:464" + }, + { + "name": "engineDamageGrace", + "value": 50, + "unit": "damage levels before thrust starts to fall", + "source": "src/DETHRACE/common/oppoproc.c:447" + }, + { + "name": "engineDamageThrustLoss", + "value": 0.0125, + "unit": "fraction of thrust per damage level", + "source": "src/DETHRACE/common/oppoproc.c:448" + }, + { + "name": "transmissionDamageThrustLoss", + "value": 0.00625, + "unit": "fraction of thrust per damage level", + "source": "src/DETHRACE/common/oppoproc.c:453" + }, + { + "name": "deadEngineThrustLoss", + "value": 0.6, + "unit": "fraction of thrust removed outright", + "source": "src/DETHRACE/common/oppoproc.c:450" + } + ], + "sources": [ + { + "path": "src/DETHRACE/common/oppoproc.c", + "line": 132 + }, + { + "path": "src/DETHRACE/common/oppoproc.c", + "line": 405 + }, + { + "path": "src/DETHRACE/common/oppoproc.c", + "line": 422 + }, + { + "path": "src/DETHRACE/common/oppoproc.c", + "line": 460 + } + ], + "capabilities": [ + "path following", + "corner speed planning", + "PD speed control", + "damage degraded driving" + ], + "tags": [ + "ai", + "driving", + "pathing", + "tuning" + ] + }, + { + "id": "chase-camera", + "node": "chase-camera", + "name": "Chase camera follow", + "summary": "The camera trails a distance derived from the zoom, keeps a damped height, and swings around behind the car", + "behavior": "The external camera sits behind the car at the square root of the zoom setting plus a small bias, so zooming out costs little at first and a great deal later. Its height is not the car's height: one height value is blended toward the car's own at a rate of five per second, and after five seconds of continuous frames it snaps to the car instead of trailing, which is what stops a long fall leaving the camera in the sky. If the car is pitching upward the camera is also pushed down by the vertical part of the facing direction, so a car climbing a ramp does not have its rear end fill the frame. The view direction is the car's facing direction while stationary and its actual velocity at speed, and a swing routine rotates that direction toward the car's heading at 0.03 degrees per millisecond, accelerating to a ceiling of 0.1 degrees per millisecond while the car is spinning, then skipping the swing altogether when the car is nearly stopped. The camera is finally aimed with a fixed downward tilt of a fifth of the field of view plus whatever extra angle is needed to look at the car's origin, and while the car is spinning the camera keeps whichever side of it the view was already on instead of swapping over.", + "constants": [ + { + "name": "distanceBias", + "value": 4, + "unit": "world units added to the square root of the zoom", + "source": "src/DETHRACE/common/car.c:5429" + }, + { + "name": "heightBias", + "value": 0.3, + "unit": "world units added to the squared zoom", + "source": "src/DETHRACE/common/car.c:5466" + }, + { + "name": "heightFollowRate", + "value": 5, + "unit": "per second", + "source": "src/DETHRACE/common/car.c:5475" + }, + { + "name": "heightSnapWindow", + "value": 5000, + "unit": "milliseconds before the height is snapped to the car", + "source": "src/DETHRACE/common/car.c:5469" + }, + { + "name": "swingRate", + "value": 0.03, + "unit": "degrees per millisecond", + "source": "src/DETHRACE/common/car.c:5631" + }, + { + "name": "swingRateCeiling", + "value": 0.1, + "unit": "degrees per millisecond", + "source": "src/DETHRACE/common/car.c:5642" + }, + { + "name": "swingExhaustion", + "value": 500, + "unit": "milliseconds of swing before the camera flips over", + "source": "src/DETHRACE/common/car.c:5583" + }, + { + "name": "movementThreshold", + "value": 0.0006, + "unit": "world units per second below which the view direction is the car's facing", + "source": "src/DETHRACE/common/car.c:5419" + }, + { + "name": "aimTiltShare", + "value": 5, + "unit": "divisor applied to the field of view for the downward tilt", + "source": "src/DETHRACE/common/car.c:5671" + } + ], + "sources": [ + { + "path": "src/DETHRACE/common/car.c", + "line": 5383 + }, + { + "path": "src/DETHRACE/common/car.c", + "line": 5429 + }, + { + "path": "src/DETHRACE/common/car.c", + "line": 5540 + }, + { + "path": "src/DETHRACE/common/car.c", + "line": 5658 + } + ], + "capabilities": [ + "trailing boom", + "damped height", + "view direction blending", + "aiming tilt" + ], + "tags": [ + "camera", + "feel", + "third person" + ] + }, + { + "id": "camera-collision", + "node": "chase-camera", + "name": "Camera collision avoidance", + "summary": "The camera shortens its boom against level geometry and other cars, and lifts itself clear when it is jammed", + "behavior": "The chase camera is never allowed to sit inside the world. From the car it sweeps a search vector a fifth longer than the actual car-to-camera offset and takes the nearest face hit; if that hit is closer than the full boom the camera is pulled in to just inside the surface plus three times the camera's near plane, and the whole test is skipped or reverted while the car is spinning rather than driving. A second pass tests a small box around the camera against the nearby faces and pushes it out along their normals, merging two nearly parallel faces first so a corner does not shove the camera twice. A separate pass tests the camera against every active car's box and, if the camera is inside one, slides it out to that box's surface inflated by three hundredths of a unit. Finally, if the shortened camera would come closer to the car than the configured minimum, it is lifted along the surface normal by a quadratic solve until it clears, and the lift is only attempted when the surface is not upside down relative to the camera, which is what keeps the car visible when the camera is jammed into a wall.", + "constants": [ + { + "name": "probeScale", + "value": 1.2, + "unit": "multiplier on the car-to-camera search vector", + "source": "src/DETHRACE/common/car.c:5761" + }, + { + "name": "hitherScale", + "value": 3, + "unit": "multiples of the near plane used as the pushout margin", + "source": "src/DETHRACE/common/car.c:5756" + }, + { + "name": "rollAvoidanceLimit", + "value": -0.7, + "unit": "surface normal Y component above which the lift is allowed", + "source": "src/DETHRACE/common/car.c:5781" + }, + { + "name": "separationMargin", + "value": 0.03, + "unit": "world units of separation kept from geometry", + "source": "src/DETHRACE/common/car.c:5802" + }, + { + "name": "parallelFaceDot", + "value": 0.95, + "unit": "normalised dot product above which two faces are merged", + "source": "src/DETHRACE/common/car.c:5832" + }, + { + "name": "carBoxMargin", + "value": 0.03, + "unit": "world units of margin kept from other cars", + "source": "src/DETHRACE/common/car.c:5896" + } + ], + "sources": [ + { + "path": "src/DETHRACE/common/car.c", + "line": 5729 + }, + { + "path": "src/DETHRACE/common/car.c", + "line": 5756 + }, + { + "path": "src/DETHRACE/common/car.c", + "line": 5802 + }, + { + "path": "src/DETHRACE/common/car.c", + "line": 5871 + } + ], + "capabilities": [ + "boom shortening", + "surface pushout", + "car avoidance", + "minimum distance lift" + ], + "tags": [ + "camera", + "collision", + "third person" + ] + }, + { + "id": "incident-camera", + "node": "incident-camera", + "name": "Incident and panning cameras", + "summary": "A second camera cuts to a nearby crash or strike, frames it from the side, and hands back when the moment is over", + "behavior": "Alongside the chase camera there is an incident camera that takes over when something worth watching happens: a pedestrian struck, a wall hit, or a car-to-car impact. For a car impact it places itself two world units from the other car along the line joining the two, raised by one unit, runs the same collision avoidance, and aims at the other car. For a pedestrian strike it steps sideways from the victim and half a body height up, picking the left or right side at random and lifting a quarter of a unit. The shot ends when two and a half seconds have passed, when the subject has moved further than five world units squared from the camera, or when a wall comes between them, and a car pinned too close to the subject returns the previous camera position rather than clipping through. A guard stops the routine from recursing into itself more than once, and when there is nothing worth cutting to it falls back to a slow panning camera that circles the car looking for an angle with a clear line of sight.", + "constants": [ + { + "name": "carSubjectOffset", + "value": 2, + "unit": "world units along the line between the two cars", + "source": "src/DETHRACE/common/car.c:5120" + }, + { + "name": "carSubjectHeight", + "value": 1, + "unit": "world units above the subject", + "source": "src/DETHRACE/common/car.c:5122" + }, + { + "name": "pedestrianHeightOffset", + "value": 0.25, + "unit": "world units above the victim", + "source": "src/DETHRACE/common/car.c:5079" + }, + { + "name": "shotLifetime", + "value": 2500, + "unit": "milliseconds", + "source": "src/DETHRACE/common/car.c:5085" + }, + { + "name": "breakDistanceSquared", + "value": 25, + "unit": "squared world units between camera and subject", + "source": "src/DETHRACE/common/car.c:5089" + }, + { + "name": "recursionLimit", + "value": 1, + "unit": "nested incident shots", + "source": "src/DETHRACE/common/car.c:5141" + }, + { + "name": "panBreakDistanceSquared", + "value": 25, + "unit": "squared world units on the panning fallback", + "source": "src/DETHRACE/common/car.c:5248" + } + ], + "sources": [ + { + "path": "src/DETHRACE/common/car.c", + "line": 5013 + }, + { + "path": "src/DETHRACE/common/car.c", + "line": 5156 + }, + { + "path": "src/DETHRACE/common/car.c", + "line": 5235 + }, + { + "path": "src/DETHRACE/common/car.c", + "line": 5141 + } + ], + "capabilities": [ + "incident framing", + "automatic cut", + "collision aware framing", + "panning fallback" + ], + "tags": [ + "camera", + "crash", + "feedback" + ] + } + ] +} diff --git a/data/games/crash-team-racing.json b/data/games/crash-team-racing.json new file mode 100644 index 0000000..7500b51 --- /dev/null +++ b/data/games/crash-team-racing.json @@ -0,0 +1,1710 @@ +{ + "version": 1, + "id": "crash-team-racing", + "title": "Crash Team Racing", + "repo": "https://github.com/CTR-tools/CTR-ModSDK", + "sha": "e3f19686fb07d62847836ae77c733573a54d3534", + "nodes": [ + { + "id": "veh-physics-core", + "name": "Kart drive physics", + "purpose": "Per-update integration for every kart on the track: throttle and braking taken from the pad, grip and counter-steer from the surface, gravity and hop launches, angular velocity from the steering ramp, and the final velocity-to-position step.", + "paths": [ + "decompile/General/Vehicle/VehPhysGeneral_0_PhysAngular.c", + "decompile/General/Vehicle/VehPhysGeneral_3_JumpGetVelY.c", + "decompile/General/Vehicle/VehPhysGeneral_4_JumpAndFriction.c", + "decompile/General/Vehicle/VehPhysGeneral_6_GetBaseSpeed.c", + "decompile/General/Vehicle/VehPhysForce_0_ConvertSpeedToVec.c", + "decompile/General/Vehicle/VehPhysForce_1_OnGravity.c", + "decompile/General/Vehicle/VehPhysForce_2_OnApplyForces.c", + "decompile/General/Vehicle/VehPhysForce_6_CounterSteer.c", + "decompile/General/Vehicle/VehPhysProc_00_Driving_PhysLinear.c", + "decompile/General/Vehicle/VehPhysProc_02_Driving_Update.c", + "decompile/General/Vehicle/VehCalc_0_InterpBySpeed.c", + "decompile/General/Vehicle/VehCalc_1_MapToRange.c", + "decompile/General/Vehicle/VehCalc_2_SteerAccel.c" + ], + "functions": [ + { + "name": "DECOMP_VehPhysGeneral_PhysAngular", + "path": "decompile/General/Vehicle/VehPhysGeneral_0_PhysAngular.c", + "lines": [ + 6, + 399 + ] + }, + { + "name": "DECOMP_VehPhysGeneral_JumpGetVelY", + "path": "decompile/General/Vehicle/VehPhysGeneral_3_JumpGetVelY.c", + "lines": [ + 3, + 34 + ] + }, + { + "name": "DECOMP_VehPhysGeneral_JumpAndFriction", + "path": "decompile/General/Vehicle/VehPhysGeneral_4_JumpAndFriction.c", + "lines": [ + 16, + 463 + ] + }, + { + "name": "DECOMP_VehPhysGeneral_GetBaseSpeed", + "path": "decompile/General/Vehicle/VehPhysGeneral_6_GetBaseSpeed.c", + "lines": [ + 3, + 95 + ] + }, + { + "name": "DECOMP_VehPhysForce_ConvertSpeedToVec", + "path": "decompile/General/Vehicle/VehPhysForce_0_ConvertSpeedToVec.c", + "lines": [ + 6, + 20 + ] + }, + { + "name": "VehPhysForce_OnGravity", + "path": "decompile/General/Vehicle/VehPhysForce_1_OnGravity.c", + "lines": [ + 3, + 35 + ] + }, + { + "name": "DECOMP_VehPhysForce_OnApplyForces", + "path": "decompile/General/Vehicle/VehPhysForce_2_OnApplyForces.c", + "lines": [ + 3, + 55 + ] + }, + { + "name": "DECOMP_VehPhysForce_CounterSteer", + "path": "decompile/General/Vehicle/VehPhysForce_6_CounterSteer.c", + "lines": [ + 3, + 28 + ] + }, + { + "name": "DECOMP_VehPhysProc_Driving_PhysLinear", + "path": "decompile/General/Vehicle/VehPhysProc_00_Driving_PhysLinear.c", + "lines": [ + 12, + 1200 + ] + }, + { + "name": "DECOMP_VehPhysProc_Driving_Update", + "path": "decompile/General/Vehicle/VehPhysProc_02_Driving_Update.c", + "lines": [ + 3, + 61 + ] + }, + { + "name": "DECOMP_VehCalc_InterpBySpeed", + "path": "decompile/General/Vehicle/VehCalc_0_InterpBySpeed.c", + "lines": [ + 3, + 21 + ] + }, + { + "name": "DECOMP_VehCalc_MapToRange", + "path": "decompile/General/Vehicle/VehCalc_1_MapToRange.c", + "lines": [ + 3, + 25 + ] + }, + { + "name": "DECOMP_VehCalc_SteerAccel", + "path": "decompile/General/Vehicle/VehCalc_2_SteerAccel.c", + "lines": [ + 3, + 46 + ] + } + ], + "dependsOn": [ + "veh-stat-tables" + ] + }, + { + "id": "veh-stat-tables", + "name": "Engine stat and physics tables", + "purpose": "One table seeds every kart with 65 handling numbers - gravity, hop impulse, three friction coefficients, free and boosted acceleration, top speed, turning rates and boost ceilings - indexed by engine class, plus the copy loop that stamps them onto a driver when it is created.", + "paths": [ + "decompile/General/zGlobal_DATA.c", + "decompile/General/Vehicle/VehBirth_3_SetConsts.c", + "mods/Levels/PS1_TrackROM/src/engine_stats.c", + "include/namespace_Vehicle.h" + ], + "functions": [ + { + "name": "DECOMP_VehBirth_SetConsts", + "path": "decompile/General/Vehicle/VehBirth_3_SetConsts.c", + "lines": [ + 7, + 42 + ] + } + ], + "dependsOn": [] + }, + { + "id": "power-slide", + "parent": "veh-physics-core", + "name": "Power-slide charge and release", + "purpose": "The drifting state: the entry condition, the boost meter that fills while the slide is held, the graded release that pays boost back, the steering that is granted inside the slide, and the spin-out that punishes holding it too long.", + "paths": [ + "decompile/General/Vehicle/VehPhysProc_09_PowerSlide_PhysAngular.c", + "decompile/General/Vehicle/VehPhysProc_10_PowerSlide_Finalize.c", + "decompile/General/Vehicle/VehPhysProc_11_PowerSlide_Update.c", + "decompile/General/Vehicle/VehPhysProc_12_PowerSlide_PhysLinear.c", + "decompile/General/Vehicle/VehPhysProc_13_PowerSlide_InitSetUpdate.c", + "decompile/General/Vehicle/VehPhysProc_14_PowerSlide_Init.c" + ], + "functions": [ + { + "name": "DECOMP_VehPhysProc_PowerSlide_PhysAngular", + "path": "decompile/General/Vehicle/VehPhysProc_09_PowerSlide_PhysAngular.c", + "lines": [ + 3, + 448 + ] + }, + { + "name": "DECOMP_VehPhysProc_PowerSlide_Finalize", + "path": "decompile/General/Vehicle/VehPhysProc_10_PowerSlide_Finalize.c", + "lines": [ + 3, + 6 + ] + }, + { + "name": "DECOMP_VehPhysProc_PowerSlide_Update", + "path": "decompile/General/Vehicle/VehPhysProc_11_PowerSlide_Update.c", + "lines": [ + 7, + 176 + ] + }, + { + "name": "DECOMP_VehPhysProc_PowerSlide_PhysLinear", + "path": "decompile/General/Vehicle/VehPhysProc_12_PowerSlide_PhysLinear.c", + "lines": [ + 3, + 7 + ] + }, + { + "name": "DECOMP_VehPhysProc_PowerSlide_InitSetUpdate", + "path": "decompile/General/Vehicle/VehPhysProc_13_PowerSlide_InitSetUpdate.c", + "lines": [ + 3, + 10 + ] + }, + { + "name": "DECOMP_VehPhysProc_PowerSlide_Init", + "path": "decompile/General/Vehicle/VehPhysProc_14_PowerSlide_Init.c", + "lines": [ + 8, + 42 + ] + } + ], + "dependsOn": [ + "veh-physics-core", + "turbo-reserves" + ] + }, + { + "id": "turbo-reserves", + "name": "Boost reserves and flame", + "purpose": "The reserve pool every boost spends, the speed ceiling a boost raises, the flame model size that reports the payout, and the per-update drain that returns the kart to its class top speed.", + "paths": [ + "decompile/General/Vehicle/VehFire_1_Increment.c", + "decompile/General/Vehicle/VehTurbo_1_ThDestroy.c", + "decompile/General/Vehicle/VehTurbo_2_ThTick.c", + "decompile/General/Vehicle/VehPhysProc_00_Driving_PhysLinear.c" + ], + "functions": [ + { + "name": "DECOMP_VehFire_Increment", + "path": "decompile/General/Vehicle/VehFire_1_Increment.c", + "lines": [ + 12, + 374 + ] + }, + { + "name": "DECOMP_VehTurbo_ThDestroy", + "path": "decompile/General/Vehicle/VehTurbo_1_ThDestroy.c", + "lines": [ + 3, + 13 + ] + }, + { + "name": "DECOMP_VehTurbo_ThTick", + "path": "decompile/General/Vehicle/VehTurbo_2_ThTick.c", + "lines": [ + 7, + 310 + ] + } + ], + "dependsOn": [ + "veh-stat-tables" + ] + }, + { + "id": "terrain-grip", + "name": "Surface and terrain grip", + "purpose": "The table that turns a level surface type into grip, counter-steer ratio, a speed cap for loose ground and the particle set for the tyres, plus the guarded lookups the kart physics calls every update.", + "paths": [ + "decompile/General/zGlobal_DATA.c", + "decompile/General/Vehicle/VehAfterColl_0_GetSurface.c", + "decompile/General/Vehicle/VehAfterColl_1_GetTerrain.c", + "include/regionsEXE.h" + ], + "functions": [ + { + "name": "DECOMP_VehAfterColl_GetSurface", + "path": "decompile/General/Vehicle/VehAfterColl_0_GetSurface.c", + "lines": [ + 3, + 10 + ] + }, + { + "name": "DECOMP_VehAfterColl_GetTerrain", + "path": "decompile/General/Vehicle/VehAfterColl_1_GetTerrain.c", + "lines": [ + 3, + 12 + ] + } + ], + "dependsOn": [] + }, + { + "id": "collision-quad", + "name": "Quadblock collision search", + "purpose": "How the world mesh answers a moving sphere: bounding-box rejection against the level collision blocks, low and high detail triangle split, and the sweeping search the kart runs each update to find the ground it stands on and the walls it scrapes.", + "paths": [ + "decompile/General/COLL/COLL_11_FIXED_QUADBLK_TestTriangles.c", + "decompile/General/COLL/COLL_12_FIXED_BSPLEAF_TestQuadblocks.c", + "decompile/General/COLL/COLL_14_FIXED_QUADBLK_GetNormVecs_HiLOD.c", + "decompile/WorkInProgress/src/COLL/COLL_MOVED_PlayerSearch.c" + ], + "functions": [ + { + "name": "COLL_FIXED_QUADBLK_TestTriangles", + "path": "decompile/General/COLL/COLL_11_FIXED_QUADBLK_TestTriangles.c", + "lines": [ + 3, + 55 + ] + }, + { + "name": "DECOMP_COLL_FIXED_BSPLEAF_TestQuadblocks", + "path": "decompile/General/COLL/COLL_12_FIXED_BSPLEAF_TestQuadblocks.c", + "lines": [ + 3, + 27 + ] + }, + { + "name": "DECOMP_COLL_FIXED_QUADBLK_GetNormVecs_HiLOD", + "path": "decompile/General/COLL/COLL_14_FIXED_QUADBLK_GetNormVecs_HiLOD.c", + "lines": [ + 3, + 46 + ] + }, + { + "name": "DECOMP_COLL_MOVED_PlayerSearch", + "path": "decompile/WorkInProgress/src/COLL/COLL_MOVED_PlayerSearch.c", + "lines": [ + 12, + 396 + ] + } + ], + "dependsOn": [] + }, + { + "id": "chase-camera", + "name": "Chase camera", + "purpose": "Places the third-person camera behind the kart: heading offset, pull-back distance derived from speed, height above the chassis, the reverse view, the look-down when the kart is hit, and the slow spin-around shot.", + "paths": [ + "decompile/General/CAMERA/CAM_15_FollowDriver_Spin360.c", + "decompile/General/CAMERA/CAM_16_SetDesiredPosRot.c", + "decompile/General/CAMERA/CAM_17_FollowDriver_Normal.c", + "decompile/General/zGlobal_DATA.c" + ], + "functions": [ + { + "name": "DECOMP_CAM_FollowDriver_Spin360", + "path": "decompile/General/CAMERA/CAM_15_FollowDriver_Spin360.c", + "lines": [ + 3, + 32 + ] + }, + { + "name": "DECOMP_CAM_SetDesiredPosRot", + "path": "decompile/General/CAMERA/CAM_16_SetDesiredPosRot.c", + "lines": [ + 3, + 20 + ] + }, + { + "name": "DECOMP_CAM_FollowDriver_Normal", + "path": "decompile/General/CAMERA/CAM_17_FollowDriver_Normal.c", + "lines": [ + 3, + 720 + ] + } + ], + "dependsOn": [ + "veh-physics-core" + ] + }, + { + "id": "ai-opponents", + "name": "Opponent driving", + "purpose": "Computer drivers follow a baked navigation path: one of three path slots per level is loaded, a heading is derived by comparing kart position with the next node, and contact between two computer karts is resolved by rank.", + "paths": [ + "decompile/General/BOTS/BOTS_00_SetGlobalNavData.c", + "decompile/General/BOTS/BOTS_01_InitNavPath.c", + "decompile/General/BOTS/BOTS_06_SetRotation.c", + "decompile/General/BOTS/BOTS_13_CollideWithOtherAI.c", + "decompile/WorkInProgress/src/BOTS/BOTS_11_ThTick_Drive.c" + ], + "functions": [ + { + "name": "DECOMP_BOTS_SetGlobalNavData", + "path": "decompile/General/BOTS/BOTS_00_SetGlobalNavData.c", + "lines": [ + 3, + 14 + ] + }, + { + "name": "DECOMP_BOTS_InitNavPath", + "path": "decompile/General/BOTS/BOTS_01_InitNavPath.c", + "lines": [ + 3, + 57 + ] + }, + { + "name": "DECOMP_BOTS_SetRotation", + "path": "decompile/General/BOTS/BOTS_06_SetRotation.c", + "lines": [ + 3, + 60 + ] + }, + { + "name": "DECOMP_BOTS_CollideWithOtherAI", + "path": "decompile/General/BOTS/BOTS_13_CollideWithOtherAI.c", + "lines": [ + 3, + 81 + ] + }, + { + "name": "BOTS_ThTick_Drive", + "path": "decompile/WorkInProgress/src/BOTS/BOTS_11_ThTick_Drive.c", + "lines": [ + 5, + 2004 + ] + } + ], + "dependsOn": [ + "veh-physics-core" + ] + }, + { + "id": "weapons-and-states", + "name": "Weapons and damage states", + "purpose": "Projectiles and thrown hazards: which victim a homing shot picks, how a hit is routed to the kart, and the timed states - spin, blast, shield hold - a kart enters before it regains control of its own physics.", + "paths": [ + "decompile/General/231/231_006_RB_Hazard_HurtDriver.c", + "decompile/General/231/231_016_RB_GenericMine_ThTick.c", + "decompile/General/231/231_055_RB_GetThread_ClosestTracker.c", + "decompile/General/Vehicle/VehPickState_0_NewState.c", + "decompile/General/Vehicle/VehPickupItem_4_ShootNow.c" + ], + "functions": [ + { + "name": "DECOMP_RB_Hazard_HurtDriver", + "path": "decompile/General/231/231_006_RB_Hazard_HurtDriver.c", + "lines": [ + 3, + 29 + ] + }, + { + "name": "DECOMP_RB_GenericMine_ThTick", + "path": "decompile/General/231/231_016_RB_GenericMine_ThTick.c", + "lines": [ + 3, + 401 + ] + }, + { + "name": "DECOMP_RB_GetThread_ClosestTracker", + "path": "decompile/General/231/231_055_RB_GetThread_ClosestTracker.c", + "lines": [ + 3, + 47 + ] + }, + { + "name": "DECOMP_VehPickState_NewState", + "path": "decompile/General/Vehicle/VehPickState_0_NewState.c", + "lines": [ + 6, + 236 + ] + }, + { + "name": "DECOMP_VehPickupItem_ShootNow", + "path": "decompile/General/Vehicle/VehPickupItem_4_ShootNow.c", + "lines": [ + 7, + 728 + ] + } + ], + "dependsOn": [ + "veh-physics-core", + "turbo-reserves" + ] + } + ], + "cards": [ + { + "id": "engine-speed-class", + "node": "veh-stat-tables", + "name": "Class top speed per engine", + "summary": "The shared physics table holds a column per engine class, and each class gets its own top speed and speedometer scale; the numbers are stored in 256ths of a kilometre per hour.", + "behavior": "Every kart is stamped with a 65-entry block of handling numbers at creation, and the entry holding top speed differs per engine class. The block carries five columns even though the retail build only exposes four of them. Speed is carried as an integer where 256 units equal one kilometre per hour, so the fastest class tops out a little over 54 km/h while the slowest sits near 50 km/h. A separate entry scales the speedometer and is larger than the class top speed for every engine, so the needle has headroom it only uses while a boost is being spent. The speed resolver finally clamps the sum of class speed, collected-fruit bonus and boost bonus, so stacking every source cannot push a kart far past the fastest class.", + "constants": [ + { + "name": "Top speed, engine 0", + "value": 13140, + "unit": "internal speed units, where 256 units are one kilometre per hour; evaluated on every update at 30 Hz (one update is 32 ms)", + "source": "decompile/General/zGlobal_DATA.c:7617" + }, + { + "name": "Top speed, engine 1", + "value": 13520, + "unit": "internal speed units, where 256 units are one kilometre per hour; evaluated on every update at 30 Hz (one update is 32 ms)", + "source": "decompile/General/zGlobal_DATA.c:7617" + }, + { + "name": "Top speed, engine 2", + "value": 13900, + "unit": "internal speed units, where 256 units are one kilometre per hour; evaluated on every update at 30 Hz (one update is 32 ms)", + "source": "decompile/General/zGlobal_DATA.c:7617" + }, + { + "name": "Top speed, engine 3", + "value": 12950, + "unit": "internal speed units, where 256 units are one kilometre per hour; evaluated on every update at 30 Hz (one update is 32 ms)", + "source": "decompile/General/zGlobal_DATA.c:7617" + }, + { + "name": "Speedometer full scale, engine 0", + "value": 14640, + "unit": "internal speed units, where 256 units are one kilometre per hour; evaluated on every update at 30 Hz (one update is 32 ms)", + "source": "decompile/General/zGlobal_DATA.c:7618" + }, + { + "name": "Speed units per kilometre per hour", + "value": 256, + "unit": "dimensionless divisor applied when a speed is shown to the player; recomputed every UI update at 30 Hz", + "source": "mods/Levels/PS1_TrackROM/src/engine_stats.c:5" + } + ], + "sources": [ + { + "path": "decompile/General/zGlobal_DATA.c", + "line": 7617 + }, + { + "path": "decompile/General/zGlobal_DATA.c", + "line": 7618 + }, + { + "path": "decompile/General/zGlobal_DATA.c", + "line": 7600 + }, + { + "path": "mods/Levels/PS1_TrackROM/src/engine_stats.c", + "line": 5 + }, + { + "path": "decompile/General/Vehicle/VehBirth_3_SetConsts.c", + "line": 18 + }, + { + "path": "decompile/General/Vehicle/VehPhysGeneral_6_GetBaseSpeed.c", + "line": 91 + } + ], + "capabilities": [ + "top-speed differencing between engine classes", + "display speed from an internal integer", + "ceiling on stacked speed bonuses" + ], + "tags": [ + "stats", + "speed", + "tuning" + ] + }, + { + "id": "acceleration-response", + "node": "veh-physics-core", + "name": "Acceleration curve", + "summary": "Throttle does not add a fixed force: free acceleration is a per-class value, spending a boost swaps in a much larger one, and standing on a boost surface substitutes a single large constant instead.", + "behavior": "Each update the drive step decides which of three accelerations to use. While the kart is on a boost surface the drive step branches to one large constant and gives up on the gentle curve, so speed climbs as fast as the cap allows. Otherwise the class acceleration value is used, and while a boost is being spent it is replaced by a value a little over twice as large. A surface that limits top speed scales the acceleration down proportionally, which is why loose ground feels sluggish rather than merely slow. The chosen acceleration is multiplied by the milliseconds that elapsed since the previous update and shifted down five places, so a frame that takes twice as long adds twice the speed instead of the same amount. The resulting delta is projected onto the kart heading and added to the velocity vector. A friction term then removes the part of the velocity magnitude that exceeds the signed base speed, capped at the acceleration this frame granted, which is what makes a kart coast back down once the throttle is released.", + "constants": [ + { + "name": "Free acceleration, engine 0", + "value": 480, + "unit": "internal speed units per update at 30 Hz, scaled by elapsed milliseconds over 32", + "source": "decompile/General/zGlobal_DATA.c:7615" + }, + { + "name": "Free acceleration, engine 1", + "value": 544, + "unit": "internal speed units per update at 30 Hz, scaled by elapsed milliseconds over 32", + "source": "decompile/General/zGlobal_DATA.c:7615" + }, + { + "name": "Free acceleration, engine 2", + "value": 448, + "unit": "internal speed units per update at 30 Hz, scaled by elapsed milliseconds over 32", + "source": "decompile/General/zGlobal_DATA.c:7615" + }, + { + "name": "Free acceleration, engine 3", + "value": 512, + "unit": "internal speed units per update at 30 Hz, scaled by elapsed milliseconds over 32", + "source": "decompile/General/zGlobal_DATA.c:7615" + }, + { + "name": "Acceleration while a boost is being spent", + "value": 1152, + "unit": "internal speed units per update at 30 Hz, scaled by elapsed milliseconds over 32", + "source": "decompile/General/zGlobal_DATA.c:7616" + }, + { + "name": "Acceleration while standing on a boost surface", + "value": 8000, + "unit": "internal speed units per update at 30 Hz, scaled by elapsed milliseconds over 32", + "source": "decompile/General/Vehicle/VehPhysGeneral_4_JumpAndFriction.c:264" + }, + { + "name": "Acceleration integration shift", + "value": 5, + "unit": "right shift applied to acceleration times elapsed milliseconds, equivalent to dividing by 32, once per 30 Hz update", + "source": "decompile/General/Vehicle/VehPhysGeneral_4_JumpAndFriction.c:267" + } + ], + "sources": [ + { + "path": "decompile/General/zGlobal_DATA.c", + "line": 7615 + }, + { + "path": "decompile/General/zGlobal_DATA.c", + "line": 7616 + }, + { + "path": "decompile/General/Vehicle/VehPhysGeneral_4_JumpAndFriction.c", + "line": 264 + }, + { + "path": "decompile/General/Vehicle/VehPhysGeneral_4_JumpAndFriction.c", + "line": 267 + }, + { + "path": "decompile/General/Vehicle/VehPhysGeneral_4_JumpAndFriction.c", + "line": 253 + } + ], + "capabilities": [ + "per-class acceleration feel", + "boost-surface speed ramp", + "frame-rate independent speed gain" + ], + "tags": [ + "physics", + "acceleration", + "tuning" + ] + }, + { + "id": "jump-and-gravity", + "node": "veh-physics-core", + "name": "Hop impulse and gravity", + "summary": "A hop is a single vertical impulse added on the first update of the jump, after which gravity is the only vertical force; both are constants per engine class, and one flag on the block underneath scales gravity down.", + "behavior": "Pressing the hop button does not start an animation that carries the kart upward - it hands the kart one upward velocity value on the first update and then gets out of the way. Every update afterwards subtracts a gravity constant from the vertical velocity, and that subtraction is scaled by elapsed milliseconds so a slow frame falls further. Blocks flagged for reduced gravity replace the constant with a fraction of it, which is how the floaty sections of a track are authored without touching the kart. A scripted or hazard-driven hop is expressed as a multiple of the same impulse, so a forced launch of three times the normal strength uses the identical code path. Horizontal speed is untouched by the hop: only the vertical component of the velocity vector changes, which keeps the line the kart takes through the air predictable.", + "constants": [ + { + "name": "Hop impulse", + "value": 4596, + "unit": "internal velocity units in 8.8 fixed point, so 256 units move the kart one world unit per update; added once per update at 30 Hz; written into the vertical velocity on the first update of the hop only", + "source": "decompile/General/zGlobal_DATA.c:7607" + }, + { + "name": "Downward acceleration", + "value": 900, + "unit": "internal velocity units subtracted from vertical velocity per update at 30 Hz, scaled by elapsed milliseconds over 32", + "source": "decompile/General/zGlobal_DATA.c:7606" + }, + { + "name": "Reduced-gravity surface scale", + "value": 41, + "unit": "percent of the normal gravity constant, applied on every update while the kart rests on a low-gravity surface", + "source": "decompile/General/Vehicle/VehPhysForce_1_OnGravity.c:17" + }, + { + "name": "Gravity fraction denominator", + "value": 100, + "unit": "percent, the denominator of the reduced-gravity fraction; evaluated on every update at 30 Hz", + "source": "decompile/General/Vehicle/VehPhysForce_1_OnGravity.c:17" + }, + { + "name": "Forced hop multiplier", + "value": 3, + "unit": "multiple of the hop impulse, applied once on the update a forced hop begins", + "source": "decompile/General/Vehicle/VehPhysGeneral_4_JumpAndFriction.c:204" + } + ], + "sources": [ + { + "path": "decompile/General/zGlobal_DATA.c", + "line": 7606 + }, + { + "path": "decompile/General/zGlobal_DATA.c", + "line": 7607 + }, + { + "path": "decompile/General/Vehicle/VehPhysForce_1_OnGravity.c", + "line": 17 + }, + { + "path": "decompile/General/Vehicle/VehPhysGeneral_4_JumpAndFriction.c", + "line": 181 + }, + { + "path": "decompile/General/Vehicle/VehPhysGeneral_4_JumpAndFriction.c", + "line": 204 + } + ], + "capabilities": [ + "single-impulse hopping", + "constant downward acceleration", + "per-surface gravity scaling", + "scripted launch strength" + ], + "tags": [ + "jump", + "physics", + "gravity" + ] + }, + { + "id": "ramp-launch-vector", + "node": "veh-physics-core", + "name": "Ramp launch from surface normals", + "summary": "Driving off a sloped face converts forward speed into upward speed by projecting the velocity onto the triangle normal, so a flat face launches nothing and a steep face launches hard.", + "behavior": "When a kart leaves the ground the engine looks at the normal of the triangle it just left and asks how much of the kart horizontal velocity points along that normal. A face with no horizontal gradient has a vertical normal only, the projection collapses to zero, and the kart simply falls. A face tilted along the direction of travel returns a large positive number, and dividing that by the vertical component of the normal turns it into an upward velocity in the same units as the rest of the physics. Faces so nearly flat that the projected value is under a small threshold are treated as flat, which stops the kart from being nudged upward by seams between blocks. The larger of that ramp projection and the surface-normal jump projection is then combined with the hop impulse in quadrature, so hopping just before a ramp adds to the launch instead of discarding it. A per-level ceiling caps the launch and can be raised or lowered by the level, and the vertical velocity is clamped upward to the chosen value rather than added to.", + "constants": [ + { + "name": "Minimum normal tilt that launches", + "value": 20, + "unit": "units of the surface normal vertical component; compared on the update the kart leaves the face", + "source": "decompile/General/Vehicle/VehPhysGeneral_3_JumpGetVelY.c:14" + }, + { + "name": "Default launch ceiling", + "value": "0x3700", + "unit": "internal vertical velocity units, applied once on the update of the launch when the level does not override it", + "source": "decompile/General/Vehicle/VehPhysGeneral_4_JumpAndFriction.c:413" + }, + { + "name": "Maximum launch ceiling", + "value": "0x5000", + "unit": "internal vertical velocity units, the upper bound a level may author; applied once on the update of the launch", + "source": "decompile/General/Vehicle/VehPhysGeneral_4_JumpAndFriction.c:416" + } + ], + "sources": [ + { + "path": "decompile/General/Vehicle/VehPhysGeneral_3_JumpGetVelY.c", + "line": 14 + }, + { + "path": "decompile/General/Vehicle/VehPhysGeneral_3_JumpGetVelY.c", + "line": 34 + }, + { + "path": "decompile/General/Vehicle/VehPhysGeneral_4_JumpAndFriction.c", + "line": 411 + }, + { + "path": "decompile/General/Vehicle/VehPhysGeneral_4_JumpAndFriction.c", + "line": 413 + }, + { + "path": "decompile/General/Vehicle/VehPhysGeneral_4_JumpAndFriction.c", + "line": 423 + } + ], + "capabilities": [ + "ramp launches from geometry alone", + "flat-face rejection", + "per-level launch strength" + ], + "tags": [ + "jump", + "ramp", + "physics" + ] + }, + { + "id": "steering-response", + "node": "veh-physics-core", + "name": "Steering rate ramp", + "summary": "Steering is an angular velocity that ramps up over a handful of frames, holds briefly, then decays; the class turn stat sets the ceiling and the surface friction scales how fast it is reached.", + "behavior": "Holding a direction does not snap the kart to a turn rate. The requested rate from the stick is scaled by the class turn stat and by the turning multiplier, then the current angular rate is moved toward it in fixed steps whose size comes from the class turn-input delay and the surface friction. Steering is therefore slower to bite on low-grip ground even though the ceiling is the same. A separate ramp modulates how much steering authority has been granted since the stick went over: it climbs across the first few frames, holds through a short plateau, then falls away over a longer window, which is what makes a quick tap feel sharp and a long hold feel stable. Once the window has passed the ramp contributes nothing and the angular rate simply stays where it is. Braking and accelerating together halves the rate ceiling and lengthens the steps, so a kart cannot be steered as tightly under combined input.", + "constants": [ + { + "name": "Turning stat, engine 0", + "value": 28, + "unit": "steering ceiling scale in internal angular-rate units; consumed on every update at 30 Hz", + "source": "decompile/General/zGlobal_DATA.c:7624" + }, + { + "name": "Turning stat, engine 1", + "value": 26, + "unit": "steering ceiling scale in internal angular-rate units; consumed on every update at 30 Hz", + "source": "decompile/General/zGlobal_DATA.c:7624" + }, + { + "name": "Turning stat, engine 2", + "value": 24, + "unit": "steering ceiling scale in internal angular-rate units; consumed on every update at 30 Hz", + "source": "decompile/General/zGlobal_DATA.c:7624" + }, + { + "name": "Turning stat, engine 3", + "value": 30, + "unit": "steering ceiling scale in internal angular-rate units; consumed on every update at 30 Hz", + "source": "decompile/General/zGlobal_DATA.c:7624" + }, + { + "name": "Backward turn rate", + "value": 48, + "unit": "internal angular-rate units at which turning is taken away as the stick swings to the other side; evaluated on every update at 30 Hz", + "source": "decompile/General/zGlobal_DATA.c:7625" + }, + { + "name": "Turn decrease rate, engine 0", + "value": 4080, + "unit": "far end of the map that converts turn state into a speed reduction, in internal speed units; consulted on every 30 Hz update", + "source": "decompile/General/zGlobal_DATA.c:7626" + }, + { + "name": "Turn input delay, engine 0", + "value": 5000, + "unit": "angular-rate units added per update toward the requested rate at 30 Hz, scaled by surface friction", + "source": "decompile/General/zGlobal_DATA.c:7627" + }, + { + "name": "Steering ramp, first stage length", + "value": 4, + "unit": "frames at 30 Hz during which granted steering climbs from nothing", + "source": "decompile/General/zGlobal_DATA.c:7632" + }, + { + "name": "Steering ramp, second stage length", + "value": 8, + "unit": "frames at 30 Hz of held maximum authority after the first stage", + "source": "decompile/General/zGlobal_DATA.c:7633" + }, + { + "name": "Steering ramp, fourth stage start", + "value": 64, + "unit": "frames at 30 Hz after which granted authority has decayed to nothing", + "source": "decompile/General/zGlobal_DATA.c:7631" + }, + { + "name": "Maximum granted steer acceleration", + "value": 3072, + "unit": "angular-rate units per frame, the plateau the ramp holds at 30 Hz", + "source": "decompile/General/zGlobal_DATA.c:7634" + }, + { + "name": "Turn-rate step while below the request", + "value": 3000, + "unit": "angular-rate units per unit of elapsed time, added on every update at 30 Hz while the rate climbs toward the stick", + "source": "decompile/General/zGlobal_DATA.c:7652" + }, + { + "name": "Turn-rate step while above the request", + "value": 500, + "unit": "angular-rate units per unit of elapsed time, subtracted on every update at 30 Hz while the rate falls back", + "source": "decompile/General/zGlobal_DATA.c:7653" + } + ], + "sources": [ + { + "path": "decompile/General/zGlobal_DATA.c", + "line": 7624 + }, + { + "path": "decompile/General/zGlobal_DATA.c", + "line": 7625 + }, + { + "path": "decompile/General/zGlobal_DATA.c", + "line": 7626 + }, + { + "path": "decompile/General/zGlobal_DATA.c", + "line": 7627 + }, + { + "path": "decompile/General/zGlobal_DATA.c", + "line": 7632 + }, + { + "path": "decompile/General/Vehicle/VehCalc_2_SteerAccel.c", + "line": 17 + }, + { + "path": "decompile/General/Vehicle/VehPhysGeneral_0_PhysAngular.c", + "line": 176 + }, + { + "path": "decompile/General/Vehicle/VehPhysGeneral_0_PhysAngular.c", + "line": 229 + } + ], + "capabilities": [ + "steering that ramps instead of snapping", + "grip-dependent steering response", + "reduced authority under brake and throttle", + "per-class turn rates" + ], + "tags": [ + "steering", + "physics", + "game-feel" + ] + }, + { + "id": "velocity-and-counter-steer", + "node": "veh-physics-core", + "name": "Velocity from heading and counter-steer", + "summary": "A single signed speed plus two angles becomes a three-dimensional velocity vector each update, and a lateral acceleration derived from the surface ratio pushes back against the direction the kart is turning.", + "behavior": "The kart keeps one scalar speed rather than a free 3D vector. Each update that scalar is multiplied by the sine and cosine of the yaw angle to get a horizontal component, which is then split again by the sine and cosine of the pitch, producing the three world-space velocity terms. Because the scalar is signed, reversing simply flips every component at once. Alongside that, a counter-steer force is computed from the difference between the current and previous steering angle, clamped to a per-kart limit and looked up through a sine table, then scaled by the surface counter-steer ratio and a small negative strength constant. The force is rotated into world space and added to the acceleration buffer, so a hard surface pushes back harder than a slippery one and a ratio of zero disables the effect entirely. The effect is skipped below a small speed, while warping, while crashed and while the chassis is not touching ground, which keeps it out of the recovery states.", + "constants": [ + { + "name": "Counter-steer strength", + "value": -8, + "unit": "internal acceleration units in 8.8 fixed point per unit of surface ratio, added on every 30 Hz update", + "source": "decompile/General/Vehicle/VehPhysForce_6_CounterSteer.c:22" + }, + { + "name": "Counter-steer dead zone", + "value": 3, + "unit": "kilometres per hour, held in 256ths internally; tested on every 30 Hz update", + "source": "decompile/General/Vehicle/VehPhysForce_6_CounterSteer.c:10" + }, + { + "name": "Counter-steer ratio, plain road", + "value": 256, + "unit": "8.8 fixed-point surface ratio where 256 is full strength and 0 disables the effect, consumed on every update at 30 Hz", + "source": "decompile/General/zGlobal_DATA.c:7358" + }, + { + "name": "Counter-steer ratio, ice", + "value": 0, + "unit": "8.8 fixed-point surface ratio where 256 is full strength and 0 disables the effect, consumed on every update at 30 Hz", + "source": "decompile/General/zGlobal_DATA.c:7424" + }, + { + "name": "Chassis radius used to find the model centre", + "value": 25, + "unit": "world units along the kart up axis, applied on every 30 Hz update before forces are integrated", + "source": "decompile/General/Vehicle/VehPhysForce_2_OnApplyForces.c:12" + }, + { + "name": "Mud sink floor", + "value": -1, + "unit": "world units of height, held in 4.12 fixed point; the depth at which sinking stops, tested on every 30 Hz update", + "source": "decompile/General/Vehicle/VehPhysForce_2_OnApplyForces.c:5" + } + ], + "sources": [ + { + "path": "decompile/General/Vehicle/VehPhysForce_0_ConvertSpeedToVec.c", + "line": 17 + }, + { + "path": "decompile/General/Vehicle/VehPhysForce_6_CounterSteer.c", + "line": 22 + }, + { + "path": "decompile/General/Vehicle/VehPhysForce_6_CounterSteer.c", + "line": 17 + }, + { + "path": "decompile/General/Vehicle/VehPhysForce_2_OnApplyForces.c", + "line": 12 + }, + { + "path": "decompile/General/zGlobal_DATA.c", + "line": 7358 + } + ], + "capabilities": [ + "signed-speed locomotion", + "surface-driven counter-steer", + "mud sinking" + ], + "tags": [ + "physics", + "vectors", + "surface" + ] + }, + { + "id": "power-slide-charge", + "node": "power-slide", + "name": "Power-slide charge and graded release", + "summary": "Holding a hop button through a hard turn starts a slide with a fixed-length boost meter; the meter fills with elapsed time, and releasing it in the last half second pays boost in up to three chained steps.", + "behavior": "A slide begins when the stick is far enough over, the hop button is down, the kart is off the brake and the speed is above half the class top speed. The slide installs its own set of update and angle functions, resets a boost meter to full length and starts two counters recording how long the slide has run. While the button is held the meter drains with elapsed milliseconds; when it reaches empty a chime fires and the driver is locked out of any further charge until the button is released. Releasing while the meter is in its last stretch converts the remaining length into reserves through a linear map, so releasing exactly as the meter empties gives the largest payout and releasing early gives less. The payout also scales with how many chained boosts have already been taken in the same slide, and each chain step raises the flame level used for the effect, up to three in a row. Releasing while the meter is still in its green stretch gives nothing but a small visual reset. Once the reserves are granted the flame persists for a fixed slice of elapsed time rather than a fixed number of frames.", + "constants": [ + { + "name": "Boost meter length", + "value": 30, + "unit": "meter units of 32 ms each, so a full bar takes one second of sliding at 30 Hz", + "source": "decompile/General/zGlobal_DATA.c:7662" + }, + { + "name": "Release window length", + "value": 15, + "unit": "meter units of 32 ms each at 30 Hz, measured back from the empty end; the graded release region", + "source": "decompile/General/zGlobal_DATA.c:7663" + }, + { + "name": "Chained boosts per slide", + "value": 3, + "unit": "boosts, counted on every 30 Hz update while the slide is held", + "source": "decompile/General/Vehicle/VehPhysProc_11_PowerSlide_Update.c:24" + }, + { + "name": "Flame level step per chain", + "value": 6, + "unit": "left shift applied to the chain counter when the boost fires; evaluated on the update of the release at 30 Hz", + "source": "decompile/General/Vehicle/VehPhysProc_11_PowerSlide_Update.c:101" + }, + { + "name": "Chained flame persistence", + "value": 15, + "unit": "multiples of 32 ms, so about 480 ms of flame at 30 Hz", + "source": "decompile/General/zGlobal_DATA.c:7665" + }, + { + "name": "Meter storage shift", + "value": 5, + "unit": "left shift converting meter units to 32 ms ticks; applied once when the slide starts", + "source": "decompile/General/Vehicle/VehPhysProc_14_PowerSlide_Init.c:29" + }, + { + "name": "Slide entry speed floor", + "value": 1, + "unit": "halving shift applied to the class top speed, sampled on every 30 Hz update", + "source": "decompile/General/Vehicle/VehPhysProc_02_Driving_Update.c:28" + } + ], + "sources": [ + { + "path": "decompile/General/zGlobal_DATA.c", + "line": 7662 + }, + { + "path": "decompile/General/zGlobal_DATA.c", + "line": 7663 + }, + { + "path": "decompile/General/Vehicle/VehPhysProc_11_PowerSlide_Update.c", + "line": 88 + }, + { + "path": "decompile/General/Vehicle/VehPhysProc_11_PowerSlide_Update.c", + "line": 114 + }, + { + "path": "decompile/General/Vehicle/VehPhysProc_14_PowerSlide_Init.c", + "line": 29 + }, + { + "path": "decompile/General/Vehicle/VehPhysProc_02_Driving_Update.c", + "line": 17 + } + ], + "capabilities": [ + "timed drift charge meter", + "graded boost release", + "chained boost escalation", + "flame persistence in milliseconds" + ], + "tags": [ + "drift", + "boost", + "game-feel" + ] + }, + { + "id": "drift-spinout-limit", + "node": "power-slide", + "name": "Drift angle and spin-out limit", + "summary": "A slide is also a steering resource: the longer it is held the more steering is granted, and holding it past a fixed count of updates spins the kart out and takes the controls away.", + "behavior": "Inside a slide the angular rate is built from two parts: a mapped term that grows with time spent sliding and rewards the driver with sharper turning, plus a term from the stick that is scaled by one of two different steering speeds depending on whether the stick agrees or disagrees with the slide direction. That second term is what makes leaning into a slide different from fighting it. A counter records how long the slide has been held in the current direction; swinging to the other side runs it back toward zero before it starts accumulating on the other sign, and with no stick input it is interpolated back to neutral one unit at a time. The moment the counter passes the class drift limit the controls are locked out and a spin state takes over, and swinging the other way past the negative of the same limit does the same, so a switchback slide has its own budget. Past half the limit the game starts a near-spin warning: a small oscillating offset is added to the kart heading and the tyres make noise, which is the cue to release. Slide time is also capped so the steering reward saturates rather than growing without bound.", + "constants": [ + { + "name": "Slide updates before spin-out", + "value": 60, + "unit": "updates at 30 Hz, so two seconds of continuous sliding in one direction", + "source": "decompile/General/zGlobal_DATA.c:7651" + }, + { + "name": "Slide time cap", + "value": 60, + "unit": "multiples of 32 ms, so the steering reward saturates after about 1.9 seconds of slide time at 30 Hz", + "source": "decompile/General/zGlobal_DATA.c:7650" + }, + { + "name": "Near-spin warning point", + "value": 1, + "unit": "right shift applied to the slide limit, so the warning begins at half the spin-out count; checked on every 30 Hz update", + "source": "decompile/General/Vehicle/VehPhysProc_09_PowerSlide_PhysAngular.c:339" + }, + { + "name": "Spin-out control lockout", + "value": "0x3c0", + "unit": "internal ticks, 32 of which pass per 30 Hz update, so 960 ticks is one second; ~1 second of no steering input at 30 Hz", + "source": "decompile/General/Vehicle/VehPhysProc_11_PowerSlide_Update.c:134" + }, + { + "name": "Drift boost pitch rate", + "value": 36, + "unit": "multiples of elapsed milliseconds over 32, added to the kart pitch axis on every update at 30 Hz while a drift boost is alive", + "source": "decompile/General/zGlobal_DATA.c:7666" + }, + { + "name": "Slide camera spin rate", + "value": 384, + "unit": "target yaw offset for the camera during a slide, in 4096-per-turn units; re-evaluated on every update at 30 Hz", + "source": "decompile/General/zGlobal_DATA.c:7654" + } + ], + "sources": [ + { + "path": "decompile/General/zGlobal_DATA.c", + "line": 7650 + }, + { + "path": "decompile/General/zGlobal_DATA.c", + "line": 7651 + }, + { + "path": "decompile/General/Vehicle/VehPhysProc_09_PowerSlide_PhysAngular.c", + "line": 339 + }, + { + "path": "decompile/General/Vehicle/VehPhysProc_09_PowerSlide_PhysAngular.c", + "line": 245 + }, + { + "path": "decompile/General/Vehicle/VehPhysProc_11_PowerSlide_Update.c", + "line": 137 + }, + { + "path": "decompile/General/Vehicle/VehPhysProc_11_PowerSlide_Update.c", + "line": 134 + } + ], + "capabilities": [ + "drift hold limit", + "switchback slide budget", + "near-spin warning cue", + "spin-out on overholding" + ], + "tags": [ + "drift", + "risk-reward", + "game-feel" + ] + }, + { + "id": "turbo-reserves-and-flame", + "node": "turbo-reserves", + "name": "Reserve pool and flame payout", + "summary": "Every boost in the game writes into the same reserve pool and raises the same speed ceiling in fixed steps; the flame model size reports which step was reached and the pool drains with elapsed time.", + "behavior": "A boost is not a fixed-duration state. It adds a quantity of reserve and raises a speed ceiling that lasts until the reserve runs out, which is why a long slide chain or a boost pad followed by another pad stacks so well. The ceiling is built from a base single-boost value plus eight units per flame level, so the payout is graded rather than binary. The flame model is chosen from the flame level with a size floor of four and a ceiling of eight, so the visual is clamped while the arithmetic is not. Reserves are decremented by the elapsed milliseconds of each update, and when they reach nothing the speed ceiling is recomputed back down to the class value, which is what produces the gentle fall back to normal speed. Boosts that come from a pad top up an outside timer instead of the reserve pool, so sitting on a pad keeps the effect alive frame by frame instead of stacking a single large amount.", + "constants": [ + { + "name": "Single-boost speed ceiling", + "value": 2048, + "unit": "internal speed units, where 256 units are one kilometre per hour; evaluated on every update at 30 Hz (one update is 32 ms); added to the class speed while reserves remain", + "source": "decompile/General/zGlobal_DATA.c:7619" + }, + { + "name": "Strongest boost speed ceiling", + "value": 4096, + "unit": "internal speed units, where 256 units are one kilometre per hour; evaluated on every update at 30 Hz (one update is 32 ms); the ceiling reachable only by the top boost tier", + "source": "decompile/General/zGlobal_DATA.c:7620" + }, + { + "name": "Reserve speed ceiling", + "value": 5120, + "unit": "internal speed units, where 256 units are one kilometre per hour; evaluated on every update at 30 Hz (one update is 32 ms); the cap applied to the reserve contribution itself", + "source": "decompile/General/zGlobal_DATA.c:7621" + }, + { + "name": "Speed added per flame level", + "value": 8, + "unit": "internal speed units added to the ceiling per flame level; recomputed on the update the boost fires at 30 Hz", + "source": "decompile/General/Vehicle/VehFire_1_Increment.c:256" + }, + { + "name": "Flame model size floor", + "value": 4, + "unit": "eighths of the model scale, clamped on every update at 30 Hz", + "source": "decompile/General/Vehicle/VehTurbo_2_ThTick.c:90" + }, + { + "name": "Flame model size ceiling", + "value": 8, + "unit": "eighths of the model scale, clamped on every update at 30 Hz", + "source": "decompile/General/Vehicle/VehTurbo_2_ThTick.c:86" + }, + { + "name": "Flame level shift for model size", + "value": 6, + "unit": "right shift converting flame level to a model size step, applied once when the flame is created", + "source": "decompile/General/Vehicle/VehFire_1_Increment.c:304" + } + ], + "sources": [ + { + "path": "decompile/General/zGlobal_DATA.c", + "line": 7619 + }, + { + "path": "decompile/General/zGlobal_DATA.c", + "line": 7620 + }, + { + "path": "decompile/General/zGlobal_DATA.c", + "line": 7621 + }, + { + "path": "decompile/General/Vehicle/VehFire_1_Increment.c", + "line": 246 + }, + { + "path": "decompile/General/Vehicle/VehFire_1_Increment.c", + "line": 328 + }, + { + "path": "decompile/General/Vehicle/VehTurbo_2_ThTick.c", + "line": 85 + }, + { + "path": "decompile/General/Vehicle/VehPhysProc_00_Driving_PhysLinear.c", + "line": 96 + } + ], + "capabilities": [ + "pooled boost reserves", + "graded speed ceilings", + "flame size from payout", + "milliseconds-based drain" + ], + "tags": [ + "boost", + "tuning", + "effects" + ] + }, + { + "id": "surface-grip-table", + "node": "terrain-grip", + "name": "Surface grip and speed caps", + "summary": "Each surface type carries its own steering friction, an optional top-speed cap, a counter-steer ratio and its tyre particles, and the kart reads the record for the block it is standing on every update.", + "behavior": "The physics never asks what a surface looks like, only for a record indexed by a small surface id. That record holds a friction value where 256 is neutral, a speed cap the kart is dragged down to on loose ground, a counter-steer ratio used by the lateral force, a lean multiplier for the drift camera and two particle emitters for the tyres. Plain road, stone and hardpack all use neutral friction and no cap, so on those the surface alone never changes how the kart feels. Ice is the outlier: friction falls to an eighth, so every steering step taken on it is an eighth of the normal one, the speed cap drops, and the lean multiplier is halved on top. Loose ground such as dirt and mud works the other way and keeps full friction while capping the speed, which is why a kart there crawls instead of sliding. Mud also sinks the chassis: the force step clamps the vertical velocity so the kart settles toward a floor instead of falling through it. A separate short table describes what happens on contact with a wall or a soft obstacle, and both lookups fall back to the first entry when handed an unknown id.", + "constants": [ + { + "name": "Road steering friction", + "value": 256, + "unit": "8.8 fixed-point multiplier where 256 is neutral, scaling the steering-rate step on every update at 30 Hz", + "source": "decompile/General/zGlobal_DATA.c:7360" + }, + { + "name": "Dirt speed cap", + "value": 180, + "unit": "internal speed units, where 256 units are one kilometre per hour; evaluated on every update at 30 Hz (one update is 32 ms); the kart is held at or below this while in contact, checked every 30 Hz update", + "source": "decompile/General/zGlobal_DATA.c:7369" + }, + { + "name": "Ice steering friction", + "value": 32, + "unit": "8.8 fixed-point multiplier where 256 is neutral, scaling the steering-rate step on every update at 30 Hz", + "source": "decompile/General/zGlobal_DATA.c:7426" + }, + { + "name": "Ice speed cap", + "value": 100, + "unit": "internal speed units, where 256 units are one kilometre per hour; evaluated on every update at 30 Hz (one update is 32 ms); the ceiling while on ice, checked every 30 Hz update", + "source": "decompile/General/zGlobal_DATA.c:7424" + }, + { + "name": "Ice forward-lean multiplier", + "value": 128, + "unit": "8.8 fixed-point multiplier where 256 is neutral, scaling the kart forward-lean target on every update at 30 Hz", + "source": "decompile/General/zGlobal_DATA.c:7426" + }, + { + "name": "Mud speed cap", + "value": 180, + "unit": "internal speed units, where 256 units are one kilometre per hour; evaluated on every update at 30 Hz (one update is 32 ms); the kart is held at or below this while in contact, checked every 30 Hz update", + "source": "decompile/General/zGlobal_DATA.c:7512" + }, + { + "name": "Surface record count", + "value": "0x15", + "unit": "surface records in the table, indexed at most once per 30 Hz update per kart", + "source": "include/regionsEXE.h:2546" + } + ], + "sources": [ + { + "path": "decompile/General/zGlobal_DATA.c", + "line": 7352 + }, + { + "path": "decompile/General/zGlobal_DATA.c", + "line": 7421 + }, + { + "path": "decompile/General/Vehicle/VehAfterColl_1_GetTerrain.c", + "line": 8 + }, + { + "path": "decompile/General/Vehicle/VehAfterColl_0_GetSurface.c", + "line": 7 + }, + { + "path": "include/regionsEXE.h", + "line": 68 + } + ], + "capabilities": [ + "per-surface steering friction", + "speed caps on loose ground", + "surface-specific tyre particles", + "fallback for unknown surfaces" + ], + "tags": [ + "surface", + "traction", + "tuning" + ] + }, + { + "id": "chassis-collision-sphere", + "node": "collision-quad", + "name": "Chassis collision sphere and triangle split", + "summary": "The kart is a sphere of a fixed radius swept through the level mesh; the search rejects blocks by bounding box, then splits each candidate into two or eight triangles depending on how many players are on screen.", + "behavior": "Collision starts from a sphere around the chassis with a radius chosen to fit the kart, and the same radius is handed both to the generic search and to the block search so the two never disagree. A block is skipped outright unless its bounding box overlaps the sphere box on all three axes, which removes almost the entire mesh before any triangle maths runs. Candidates are then split differently by player count: with one or two players each block is split into eight triangles for a fine-grained normal, while with three or four players only two triangles are used, trading shape accuracy for the extra cars on screen. A flat quad is tested once as two triangles, but a block whose fourth vertex differs is tested as two halves so the fold is respected. Triangle tests are skipped unless the block carries one of the wanted layer bits and none of the ignored ones, which is how a search asks for ground only, walls only, or both.", + "constants": [ + { + "name": "Chassis collision radius", + "value": 25, + "unit": "world units; swept and re-tested on every 30 Hz update", + "source": "decompile/WorkInProgress/src/COLL/COLL_MOVED_PlayerSearch.c:28" + }, + { + "name": "Wanted block layers", + "value": "0x3000", + "unit": "bit mask of the ground and wall layers, tested on every block examined each 30 Hz update", + "source": "decompile/WorkInProgress/src/COLL/COLL_MOVED_PlayerSearch.c:42" + }, + { + "name": "Player count that switches to coarse blocks", + "value": 3, + "unit": "karts on screen; the detail level is chosen once per update at 30 Hz", + "source": "decompile/WorkInProgress/src/COLL/COLL_MOVED_PlayerSearch.c:50" + }, + { + "name": "Coarse triangles per block", + "value": 2, + "unit": "triangles tested per candidate block, per 30 Hz update", + "source": "decompile/WorkInProgress/src/COLL/COLL_MOVED_PlayerSearch.c:46" + }, + { + "name": "Fine triangles per block", + "value": 8, + "unit": "triangles tested per candidate block, per 30 Hz update", + "source": "decompile/WorkInProgress/src/COLL/COLL_MOVED_PlayerSearch.c:52" + } + ], + "sources": [ + { + "path": "decompile/WorkInProgress/src/COLL/COLL_MOVED_PlayerSearch.c", + "line": 28 + }, + { + "path": "decompile/WorkInProgress/src/COLL/COLL_MOVED_PlayerSearch.c", + "line": 42 + }, + { + "path": "decompile/General/COLL/COLL_11_FIXED_QUADBLK_TestTriangles.c", + "line": 26 + }, + { + "path": "decompile/General/COLL/COLL_14_FIXED_QUADBLK_GetNormVecs_HiLOD.c", + "line": 16 + }, + { + "path": "include/ctr/lev.h", + "line": 70 + } + ], + "capabilities": [ + "sphere-versus-mesh collision", + "bounding-box rejection", + "level-of-detail by player count", + "layer masking for ground and walls" + ], + "tags": [ + "collision", + "level", + "performance" + ] + }, + { + "id": "chase-camera-framing", + "node": "chase-camera", + "name": "Chase camera framing", + "summary": "The camera hangs behind the kart at a distance mapped from speed, rises a fixed height above it, and adds a half-turn offset plus the kart yaw; holding the rear-view button flips that offset by the same half turn.", + "behavior": "Framing is built from three numbers: a yaw that follows the kart heading with a fixed half-turn offset, a distance that the engine maps from the smoothed camera speed into a range between a near and a far value, and a height above the chassis. As the kart speeds up the camera is pulled back until the distance saturates, and it eases back in when the kart slows, so the framing breathes with the race instead of snapping. The pitch is nudged downward when the kart is undamaged and back to level while it is in a hit state, which is what gives a clean chase view normally and a looser view when the driver has lost control. Boost stretches the pull-back briefly and then decays, giving the sensation of acceleration without a permanent zoom-out. A transition between camera modes sets a destination and blends toward it over a full second, and the rear-view button simply adds the same half-turn used for the forward offset so the camera swings all the way around. A separate shot orbits the kart on a sine and cosine pair at a fixed radius, used when the race wants to show the kart from every side.", + "constants": [ + { + "name": "Heading half-turn offset", + "value": "0x800", + "unit": "angular units in 4096-per-turn scale, added to the kart yaw on every update at 30 Hz", + "source": "decompile/General/CAMERA/CAM_17_FollowDriver_Normal.c:210" + }, + { + "name": "Reverse view yaw flip", + "value": "0x800", + "unit": "angular units in 4096-per-turn scale, applied on the single update the rear-view button changes state", + "source": "decompile/General/CAMERA/CAM_17_FollowDriver_Normal.c:55" + }, + { + "name": "Nearest pull-back distance", + "value": "0xB3", + "unit": "world units behind the kart, chosen on every update at 30 Hz from the speed map", + "source": "decompile/General/zGlobal_DATA.c:461" + }, + { + "name": "Farthest pull-back distance", + "value": "0xE0", + "unit": "world units behind the kart, chosen on every update at 30 Hz from the speed map", + "source": "decompile/General/zGlobal_DATA.c:462" + }, + { + "name": "Camera height above the chassis", + "value": "0x40", + "unit": "world units, added on every update at 30 Hz", + "source": "decompile/General/zGlobal_DATA.c:470" + }, + { + "name": "Distance easing, retained fraction", + "value": "0xF0", + "unit": "8.8 fixed-point fraction of the previous camera distance carried into the next one, per 30 Hz update", + "source": "decompile/General/zGlobal_DATA.c:465" + }, + { + "name": "Mode transition length", + "value": 1, + "unit": "seconds; the code stores 30 updates at 30 Hz, so a new camera mode takes a second to settle", + "source": "decompile/General/CAMERA/CAM_16_SetDesiredPosRot.c:15" + }, + { + "name": "Damage pitch step", + "value": 8, + "unit": "angular units of camera pitch moved per update at 30 Hz; the frame-rate macro is an identity at 30 Hz", + "source": "decompile/General/CAMERA/CAM_17_FollowDriver_Normal.c:221" + } + ], + "sources": [ + { + "path": "decompile/General/CAMERA/CAM_17_FollowDriver_Normal.c", + "line": 210 + }, + { + "path": "decompile/General/CAMERA/CAM_17_FollowDriver_Normal.c", + "line": 127 + }, + { + "path": "decompile/General/CAMERA/CAM_17_FollowDriver_Normal.c", + "line": 55 + }, + { + "path": "decompile/General/CAMERA/CAM_16_SetDesiredPosRot.c", + "line": 15 + }, + { + "path": "decompile/General/CAMERA/CAM_15_FollowDriver_Spin360.c", + "line": 24 + }, + { + "path": "decompile/General/zGlobal_DATA.c", + "line": 459 + } + ], + "capabilities": [ + "speed-dependent chase distance", + "reverse view", + "mode blending over one second", + "orbit shot" + ], + "tags": [ + "camera", + "game-feel", + "framing" + ] + }, + { + "id": "opponent-nav-driving", + "node": "ai-opponents", + "name": "Opponent path following", + "summary": "Computer drivers do not steer by feel: they read a baked chain of navigation nodes, compare their position with the next node and turn toward the heading that difference implies.", + "behavior": "Each level supplies navigation paths in three slots, selected by an index, stored as a header plus an array of nodes. The header carries a count and a validity tag, and the loader checks the tag before trusting the count so an out-of-date path silently collapses to zero nodes instead of walking off the end of a stale array. A driver tracks its position in 8.8 fixed point and shifts it down to whole world units before comparing with the node position, so the comparison happens in whole world units, where squared distances stay small. The heading between kart and node is built from the horizontal distance and the height difference, converted to a rotation, and then offset by a half turn to point the model the other way. Two computer karts in contact are separated using race rank to decide which one is treated as the leader rather than using momentum, which keeps a pack of them from locking together.", + "constants": [ + { + "name": "Position fixed-point shift", + "value": 8, + "unit": "right shift turning 8.8 fixed-point world coordinates into whole world units; applied on every 30 Hz update", + "source": "decompile/General/BOTS/BOTS_06_SetRotation.c:13" + }, + { + "name": "Heading angle scale", + "value": "0x1000", + "unit": "vertical exaggeration factor in the heading lookup, evaluated once per 30 Hz update", + "source": "decompile/General/BOTS/BOTS_06_SetRotation.c:34" + }, + { + "name": "Heading half-turn offset", + "value": "0x800", + "unit": "angular units in 4096-per-turn scale, applied once per 30 Hz update when the node heading is stored", + "source": "decompile/General/BOTS/BOTS_06_SetRotation.c:43" + }, + { + "name": "Navigation path slots", + "value": 3, + "unit": "paths per level, one per route; resolved once when the level loads", + "source": "include/regionsEXE.h:4479" + }, + { + "name": "Path validity tag", + "value": "-0x1303", + "unit": "sentinel compared once when the path is loaded; a mismatch zeroes the node count rather than reading stale data", + "source": "decompile/General/BOTS/BOTS_01_InitNavPath.c:24" + } + ], + "sources": [ + { + "path": "decompile/General/BOTS/BOTS_01_InitNavPath.c", + "line": 24 + }, + { + "path": "decompile/General/BOTS/BOTS_06_SetRotation.c", + "line": 34 + }, + { + "path": "decompile/General/BOTS/BOTS_13_CollideWithOtherAI.c", + "line": 9 + }, + { + "path": "decompile/WorkInProgress/src/BOTS/BOTS_11_ThTick_Drive.c", + "line": 5 + }, + { + "path": "decompile/General/zGlobal_DATA.c", + "line": 120 + } + ], + "capabilities": [ + "navigation path following", + "route-specific opponent lines", + "rank-based kart separation", + "stale-path rejection" + ], + "tags": [ + "ai", + "opponents", + "paths" + ] + }, + { + "id": "weapon-impact-and-recovery", + "node": "weapons-and-states", + "name": "Hits, lockouts and shields", + "summary": "A weapon hit is routed through one entry point that refuses to touch an invulnerable or shielded kart, and otherwise installs a state whose control lockout is a different length for each damage kind.", + "behavior": "Every hazard funnels its hit through the same state picker, which first checks whether the victim is already holding a protective effect or has temporary invulnerability and, if so, plays a taunt and returns without changing anything. A held shield is not a separate code path - it is consumed inside the same function, granting a longer invulnerability window and popping the bubble before the hit is discarded. Each damage kind then maps to a concrete state and a lockout length: a spin costs about a second of control, a blast costs about two and a half seconds and hands the kart to a tumbling state, and lighter kinds return without touching the state at all. The lockout is stored in a timer that the drive step decrements by elapsed milliseconds, which is why a kart hit on a slow frame recovers on roughly the same clock time as one hit on a fast frame. Thrown hazards carry their own short cooldown after release so a dropped object does not immediately re-arm and hit the kart that dropped it.", + "constants": [ + { + "name": "Spin-out lockout", + "value": "0x3c0", + "unit": "internal ticks, 32 of which pass per 30 Hz update, so 960 ticks is one second; about one second of no input at 30 Hz", + "source": "decompile/General/Vehicle/VehPickState_0_NewState.c:56" + }, + { + "name": "Blast lockout", + "value": "0x960", + "unit": "internal ticks, 32 of which pass per 30 Hz update, so 960 ticks is one second; about two and a half seconds of no input at 30 Hz", + "source": "decompile/General/Vehicle/VehPickState_0_NewState.c:79" + }, + { + "name": "Shield invulnerability window", + "value": "0x2a0", + "unit": "internal ticks, 32 of which pass per 30 Hz update, so 960 ticks is one second; granted on the update the shield absorbs a hit", + "source": "decompile/General/Vehicle/VehPickState_0_NewState.c:40" + }, + { + "name": "Thrown hazard cooldown", + "value": "0xf0", + "unit": "internal ticks, 32 of which pass per 30 Hz update, so 960 ticks is one second; counted down by elapsed milliseconds on every 30 Hz update", + "source": "decompile/General/231/231_016_RB_GenericMine_ThTick.c:33" + }, + { + "name": "Stronger boost item fruit threshold", + "value": 10, + "unit": "collected fruit; checked once when the item is used", + "source": "decompile/General/Vehicle/VehPickupItem_4_ShootNow.c:38" + } + ], + "sources": [ + { + "path": "decompile/General/Vehicle/VehPickState_0_NewState.c", + "line": 21 + }, + { + "path": "decompile/General/Vehicle/VehPickState_0_NewState.c", + "line": 56 + }, + { + "path": "decompile/General/Vehicle/VehPickState_0_NewState.c", + "line": 79 + }, + { + "path": "decompile/General/231/231_006_RB_Hazard_HurtDriver.c", + "line": 15 + }, + { + "path": "decompile/General/231/231_016_RB_GenericMine_ThTick.c", + "line": 33 + } + ], + "capabilities": [ + "single hit entry point", + "shield consumption", + "per-hit control lockouts", + "hazard re-arm delay" + ], + "tags": [ + "combat", + "states", + "timers" + ] + }, + { + "id": "missile-targeting", + "node": "weapons-and-states", + "name": "Homing target selection", + "summary": "A homing shot reserves a slot in a fixed global budget, picks the victim by the smallest planar distance among its own projectiles, and clears the on-screen target marker when it detonates.", + "behavior": "Firing a homing projectile first checks a global counter and refuses outright once a fixed number are already in flight, which caps how many tracking objects the engine will ever simulate at once. The victim is whatever the aim step picked rather than the race leader, so the launch stores that driver as the projectile target and the projectile never re-picks. Every projectile that shares a target compares itself against the others using the squared horizontal distance between the projectile and its target, so no square root is taken during the search and the comparison is monotonic. The winner is the one closest on the ground plane, and the height difference is deliberately ignored so a projectile cannot be ignored simply because it has been thrown high. On impact the projectile kills its own thread, mutes its flight audio and clears the marker that was drawn on the victim screen, so the target display is clean again as soon as the shot lands.", + "constants": [ + { + "name": "Tracked projectiles in flight", + "value": 12, + "unit": "projectiles; the counter is checked once when a shot is fired", + "source": "decompile/General/Vehicle/VehPickupItem_4_ShootNow.c:46" + }, + { + "name": "Distance search initial value", + "value": "0x3fffffff", + "unit": "squared world units; the search sentinel re-armed on every evaluation of a projectile pair", + "source": "decompile/General/231/231_055_RB_GetThread_ClosestTracker.c:12" + } + ], + "sources": [ + { + "path": "decompile/General/Vehicle/VehPickupItem_4_ShootNow.c", + "line": 46 + }, + { + "path": "decompile/General/231/231_055_RB_GetThread_ClosestTracker.c", + "line": 37 + }, + { + "path": "decompile/General/231/231_010_RB_Hazard_ThCollide_Missile.c", + "line": 13 + }, + { + "path": "decompile/WorkInProgress/src/231/231_030_RB_Warpball_GetDriverTarget.c", + "line": 3 + } + ], + "capabilities": [ + "fixed projectile budget", + "planar distance target search", + "target marker cleanup" + ], + "tags": [ + "combat", + "projectiles", + "targeting" + ] + } + ] +} diff --git a/data/games/grand-prix-4.json b/data/games/grand-prix-4.json new file mode 100644 index 0000000..f11c86a --- /dev/null +++ b/data/games/grand-prix-4.json @@ -0,0 +1,1431 @@ +{ + "version": 1, + "id": "grand-prix-4", + "title": "Grand Prix 4", + "repo": "https://github.com/leighrobertabbott/decomp-grandprix4", + "sha": "d71cbd42dc2d358c9c716de628871177e7a90ba1", + "nodes": [ + { + "id": "sim-frame", + "name": "Frame loop and fixed-step car update", + "purpose": "The outer simulation frame: once per rendered frame it prepares the session, steps every listed car, splits the frame span into equal sub-steps, runs the per-chunk update chain and closes the frame.", + "paths": [ + "src/gp4/c/0043/004362cd.cpp", + "src/gp4/asm/0041/004198c7.cpp", + "src/gp4/c/0041/0041970f.cpp" + ], + "functions": [ + { + "name": "FUN_004362cd", + "path": "src/gp4/c/0043/004362cd.cpp", + "lines": [ + 35, + 138 + ] + }, + { + "name": "FUN_004198c7", + "path": "src/gp4/asm/0041/004198c7.cpp", + "lines": [ + 27, + 144 + ] + }, + { + "name": "FUN_0041970f", + "path": "src/gp4/c/0041/0041970f.cpp", + "lines": [ + 35, + 98 + ] + } + ], + "dependsOn": [ + "body-integration", + "tyres", + "aero", + "suspension", + "steering", + "drivetrain" + ] + }, + { + "id": "body-integration", + "parent": "sim-frame", + "name": "Sub-step body integration", + "purpose": "Advances the six degrees of freedom of a body slot (three positions, three velocities) by the whole sub-step, using a quadratic drag term plus linear damping.", + "paths": [ + "src/gp4/asm/0041/0041b035.cpp" + ], + "functions": [ + { + "name": "FUN_0041b035", + "path": "src/gp4/asm/0041/0041b035.cpp", + "lines": [ + 15, + 64 + ] + } + ], + "dependsOn": [] + }, + { + "id": "tyres", + "name": "Tyre load, slip and force", + "purpose": "Per-wheel load, the two slip inputs, the fixed-size coefficient records and the soft-saturating curve that converts slip into longitudinal and lateral force.", + "paths": [ + "src/gp4/asm/0041/0041724d.cpp", + "src/gp4/asm/0041/00411e17.cpp", + "src/gp4/c/0041/00411cc4.cpp", + "src/gp4/c/0041/00411e69.cpp", + "src/gp4/c/0041/00411f09.cpp" + ], + "functions": [ + { + "name": "FUN_0041724d", + "path": "src/gp4/asm/0041/0041724d.cpp", + "lines": [ + 65, + 160 + ] + }, + { + "name": "FUN_00411e17", + "path": "src/gp4/asm/0041/00411e17.cpp", + "lines": [ + 15, + 29 + ] + }, + { + "name": "FUN_00411cc4", + "path": "src/gp4/c/0041/00411cc4.cpp", + "lines": [ + 56, + 101 + ] + }, + { + "name": "FUN_00411e69", + "path": "src/gp4/c/0041/00411e69.cpp", + "lines": [ + 32, + 70 + ] + }, + { + "name": "FUN_00411f09", + "path": "src/gp4/c/0041/00411f09.cpp", + "lines": [ + 31, + 64 + ] + } + ], + "dependsOn": [ + "suspension", + "aero" + ] + }, + { + "id": "aero", + "name": "Aerodynamic load", + "purpose": "Blends the stored aerodynamic load pair by a height-dependent weight and spreads half of each axle value, plus a per-wheel trim, into the four wheel loads.", + "paths": [ + "src/gp4/c/0041/00416dec.cpp", + "src/gp4/c/0041/00417066.cpp" + ], + "functions": [ + { + "name": "FUN_00416dec", + "path": "src/gp4/c/0041/00416dec.cpp", + "lines": [ + 32, + 100 + ] + }, + { + "name": "FUN_00417066", + "path": "src/gp4/c/0041/00417066.cpp", + "lines": [ + 49, + 123 + ] + } + ], + "dependsOn": [] + }, + { + "id": "suspension", + "name": "Suspension travel and load transfer", + "purpose": "Turns per-wheel load into spring compression, damper rate and a ride height, and moves the difference between the axle means into the body roll term.", + "paths": [ + "src/gp4/c/0041/0041073d.cpp", + "src/gp4/c/0041/0041c145.cpp" + ], + "functions": [ + { + "name": "FUN_0041073d", + "path": "src/gp4/c/0041/0041073d.cpp", + "lines": [ + 49, + 94 + ] + }, + { + "name": "FUN_0041c145", + "path": "src/gp4/c/0041/0041c145.cpp", + "lines": [ + 68, + 105 + ] + } + ], + "dependsOn": [] + }, + { + "id": "steering", + "name": "Steering input and heading response", + "purpose": "Builds the steering value for the frame from the driver input and the track heading, rate-limits it, and provides the response curve and per-wheel steering offsets.", + "paths": [ + "src/gp4/asm/0041/0041416e.cpp", + "src/gp4/asm/0041/00415b00.cpp", + "src/gp4/asm/0041/0041b3d9.cpp" + ], + "functions": [ + { + "name": "FUN_0041416e", + "path": "src/gp4/asm/0041/0041416e.cpp", + "lines": [ + 51, + 166 + ] + }, + { + "name": "FUN_00415b00", + "path": "src/gp4/asm/0041/00415b00.cpp", + "lines": [ + 24, + 74 + ] + }, + { + "name": "FUN_0041b3d9", + "path": "src/gp4/asm/0041/0041b3d9.cpp", + "lines": [ + 42, + 70 + ] + } + ], + "dependsOn": [ + "track-ring" + ] + }, + { + "id": "drivetrain", + "name": "Engine torque and gearbox", + "purpose": "The interpolated engine torque curve, the gear ratio table, the gear-selection ramp and the percentage blend that softens a gear change.", + "paths": [ + "src/gp4/asm/0044/0044192c.cpp", + "src/gp4/asm/0041/00416487.cpp", + "src/gp4/asm/0041/0041034e.cpp", + "src/gp4/asm/0041/0041362a.cpp" + ], + "functions": [ + { + "name": "FUN_0044192c", + "path": "src/gp4/asm/0044/0044192c.cpp", + "lines": [ + 52, + 132 + ] + }, + { + "name": "FUN_00416487", + "path": "src/gp4/asm/0041/00416487.cpp", + "lines": [ + 51, + 84 + ] + }, + { + "name": "FUN_0041034e", + "path": "src/gp4/asm/0041/0041034e.cpp", + "lines": [ + 13, + 67 + ] + }, + { + "name": "FUN_0041362a", + "path": "src/gp4/asm/0041/0041362a.cpp", + "lines": [ + 40, + 68 + ] + } + ], + "dependsOn": [ + "tyres" + ] + }, + { + "id": "collision", + "name": "Car contact and collision response", + "purpose": "Swept pair tests over one frame, the oriented-box contact test built from outcode clipping, and the mass-weighted blend that separates two touching bodies.", + "paths": [ + "src/gp4/asm/0043/00436970.cpp", + "src/gp4/asm/0043/00438c98.cpp", + "src/gp4/asm/0043/00439f75.cpp" + ], + "functions": [ + { + "name": "FUN_00436970", + "path": "src/gp4/asm/0043/00436970.cpp", + "lines": [ + 110, + 235 + ] + }, + { + "name": "FUN_00438c98", + "path": "src/gp4/asm/0043/00438c98.cpp", + "lines": [ + 94, + 255 + ] + }, + { + "name": "FUN_00439f75", + "path": "src/gp4/asm/0043/00439f75.cpp", + "lines": [ + 69, + 92 + ] + } + ], + "dependsOn": [ + "sim-frame" + ] + }, + { + "id": "track-ring", + "name": "Track segment ring and lap position", + "purpose": "The circular list of fixed-size track segment records, the forward and backward walkers over it, and the projection of a car onto the lap length.", + "paths": [ + "src/gp4/asm/0040/00409467.cpp", + "src/gp4/asm/0041/0041cb9b.cpp" + ], + "functions": [ + { + "name": "FUN_00409467", + "path": "src/gp4/asm/0040/00409467.cpp", + "lines": [ + 11, + 62 + ] + }, + { + "name": "FUN_0041cb9b", + "path": "src/gp4/asm/0041/0041cb9b.cpp", + "lines": [ + 83, + 140 + ] + } + ], + "dependsOn": [] + }, + { + "id": "race-clock", + "name": "Race clock, lap digits and session timers", + "purpose": "Two fixed-point clock accumulators advanced by per-frame deltas, the remaining-time split into decimal digits, and the day and countdown deadlines.", + "paths": [ + "src/gp4/c/0044/00443393.cpp", + "src/gp4/asm/0040/0040996f.cpp", + "src/gp4/c/0044/00442cb4.cpp" + ], + "functions": [ + { + "name": "FUN_00443393", + "path": "src/gp4/c/0044/00443393.cpp", + "lines": [ + 27, + 119 + ] + }, + { + "name": "FUN_0040996f", + "path": "src/gp4/asm/0040/0040996f.cpp", + "lines": [ + 30, + 48 + ] + }, + { + "name": "FUN_00442cb4", + "path": "src/gp4/c/0044/00442cb4.cpp", + "lines": [ + 25, + 29 + ] + } + ], + "dependsOn": [ + "track-ring" + ] + }, + { + "id": "ai-driver", + "name": "AI driver line following", + "purpose": "Finds the target point ahead on the racing line, damps the car offsets and closing speed in that frame, and turns the heading toward it with a deadband and a rate cap.", + "paths": [ + "src/gp4/asm/0043/00433709.cpp", + "src/gp4/asm/0042/0042c1f4.cpp" + ], + "functions": [ + { + "name": "FUN_00433709", + "path": "src/gp4/asm/0043/00433709.cpp", + "lines": [ + 306, + 503 + ] + }, + { + "name": "FUN_0042c1f4", + "path": "src/gp4/asm/0042/0042c1f4.cpp", + "lines": [ + 512, + 613 + ] + } + ], + "dependsOn": [ + "steering", + "track-ring" + ] + }, + { + "id": "pit-lane", + "name": "Pit lane detection", + "purpose": "Decides per car and frame whether it is on the pit lane or the racing lap, which lane kind it is on, and the lateral limit that lane entry allows.", + "paths": [ + "src/gp4/asm/0043/004320f2.cpp", + "src/gp4/asm/0042/00425cb3.cpp" + ], + "functions": [ + { + "name": "FUN_004320f2", + "path": "src/gp4/asm/0043/004320f2.cpp", + "lines": [ + 49, + 150 + ] + }, + { + "name": "FUN_00425cb3", + "path": "src/gp4/asm/0042/00425cb3.cpp", + "lines": [ + 49, + 115 + ] + } + ], + "dependsOn": [ + "track-ring" + ] + }, + { + "id": "camera", + "name": "View and projection matrices", + "purpose": "Builds the perspective projection and the view matrix for a raised, position-locked viewpoint, and derives the orthonormal basis of the camera body.", + "paths": [ + "src/gp4/c/0049/00492c10.cpp", + "src/gp4/c/0040/0040233e.cpp", + "src/gp4/c/0040/004024b5.cpp" + ], + "functions": [ + { + "name": "FUN_00492c10", + "path": "src/gp4/c/0049/00492c10.cpp", + "lines": [ + 20, + 48 + ] + }, + { + "name": "FUN_0040233e", + "path": "src/gp4/c/0040/0040233e.cpp", + "lines": [ + 11, + 32 + ] + }, + { + "name": "FUN_004024b5", + "path": "src/gp4/c/0040/004024b5.cpp", + "lines": [ + 12, + 40 + ] + } + ], + "dependsOn": [] + } + ], + "cards": [ + { + "id": "substep-scheduler", + "node": "sim-frame", + "name": "Frame split into equal sub-steps", + "summary": "A rendered frame is not one physics step: its span is divided into equal sub-steps and the car chain is run once per bounded chunk, so handling stays the same at any frame rate.", + "behavior": "Each car spends its frame in a scheduler rather than a single update call. The scheduler reads the span of simulated time for the frame and divides it by the nominal sub-step length, adding half a step before the divide so the sub-step count is the nearest whole number instead of a floor. It then clears the per-frame accumulators, runs the once-per-frame preparations (load refresh, tyre and gear requests, telemetry sampling) and enters a loop that runs once per chunk: the time accumulator is reloaded with the chunk count times the nominal step, the chain of body, aerodynamic, tyre, suspension and drivetrain updates runs against that shortened span, and the once-per-chunk counters advance. Because every routine downstream reads the chunk span and not the frame span, a 15 ms frame and a 40 ms frame produce the same sequence of equal steps, which is what keeps the car from behaving differently on a slow machine. Replay playback and normal play share this split and differ only in the finisher chosen by a state byte, and the original frame span is written back when the loop ends.", + "constants": [ + { + "name": "Rounding bias in the sub-step divide", + "value": 1, + "unit": "integer sub-steps (half a step added before dividing); per frame, frame rate unverified", + "source": "src/gp4/c/0041/0041970f.cpp:45" + }, + { + "name": "Minimum sub-steps per pass", + "value": 1, + "unit": "count of sub-steps; per pass, rate unverified", + "source": "src/gp4/asm/0041/0041b035.cpp:24" + }, + { + "name": "Chunk cap in sub-steps", + "value": "0x006228b0", + "unit": "address of the 32-bit chunk cap; reloaded per chunk within an update, rate unverified", + "source": "src/gp4/asm/0041/004198c7.cpp:60" + } + ], + "sources": [ + { + "path": "src/gp4/c/0043/004362cd.cpp", + "line": 23 + }, + { + "path": "src/gp4/c/0041/0041970f.cpp", + "line": 32 + }, + { + "path": "src/gp4/asm/0041/004198c7.cpp", + "line": 22 + } + ], + "capabilities": [ + "fixed-step simulation", + "frame-rate independence", + "per-frame scheduling" + ], + "tags": [ + "physics", + "timing", + "architecture" + ] + }, + { + "id": "body-substep-integration", + "node": "body-integration", + "name": "Quadratic drag integration of body position and velocity", + "summary": "Every sub-step each body slot is pushed along three axes by a force that is quadratic in position plus linear in velocity, and its position is advanced with the mean of the old and new velocity.", + "behavior": "For a slot of the body array the routine reads one position and one velocity per axis. The three force terms are built from per-axis coefficients that are converted from scaled integers to doubles once per call, outside the loop, so a chunk of many sub-steps costs only multiplies and adds: a term that grows with the square of the offset from rest and keeps its sign, a term proportional to the current velocity, and a constant bias. The negated force scaled by the sub-step length is added to the velocity, and the position is then advanced with the average of the old and the new velocity, a midpoint scheme that stays stable for the spring-like position term. Everything between the loads and the stores stays in extended precision in the original, so the rounding happens once per axis per sub-step. Slot positions are addressed by a stride, and the coefficient triples sit just past the last slot of the table so they do not change during the loop.", + "constants": [ + { + "name": "Integrated axes per body slot", + "value": 3, + "unit": "axes; per sub-step, rate unverified", + "source": "src/gp4/asm/0041/0041b035.cpp:46" + }, + { + "name": "Body slot stride", + "value": "0x60", + "unit": "bytes per body slot, static layout, no timestep", + "source": "src/gp4/asm/0041/0041b035.cpp:19" + }, + { + "name": "Quadratic drag coefficient triple", + "value": "0x006028f4", + "unit": "address of three scaled integers read once per call; per sub-step, rate unverified", + "source": "src/gp4/asm/0041/0041b035.cpp:40" + } + ], + "sources": [ + { + "path": "src/gp4/asm/0041/0041b035.cpp", + "line": 9 + }, + { + "path": "src/gp4/asm/0041/0041b035.cpp", + "line": 52 + }, + { + "path": "src/gp4/asm/0041/0041b035.cpp", + "line": 55 + } + ], + "capabilities": [ + "rigid body integration", + "nonlinear drag", + "midpoint position update" + ], + "tags": [ + "physics", + "integration" + ] + }, + { + "id": "tyre-load-and-force", + "node": "tyres", + "name": "Per-wheel load, slip clamping and force split", + "summary": "Each of the four wheels turns its vertical load and two slip inputs into a pure slip force, a combined lateral force and a combined longitudinal force, mirrored for the right-hand side.", + "behavior": "The routine runs once per wheel with the wheel index. It first clears the slide bit of the wheel status, then forms the vertical load as a fixed-point product of a load field and a suspension term, scaled again by a global gain; both an integer load and a double version are kept, and the double is the multiplier every force is finally scaled by. Two slip inputs follow: one from a shared wheel field times a per-wheel factor, given the sign of a direction field; the other from the same wheel field, a second per-wheel term and a table entry chosen by wheel index, whose sign is flipped for the odd-numbered wheels because the four records mirror the two sides of the car. Both are clamped to a magnitude limit, so a spin or a lock-up cannot produce an unbounded input. Three curve evaluations then run on the clamped pair: one pure slip force, one combined lateral force and one combined longitudinal force; each result is rounded to an integer and written to both a per-wheel slot and a shared slot. Finally the two slip angles the curves reported are scaled and the larger magnitude is compared against a global threshold, and when it reaches that threshold the slide bit is set for the rest of the frame, which the visual and audio layers read as a tyre in trouble.", + "constants": [ + { + "name": "Wheel record stride", + "value": "0x300", + "unit": "bytes per wheel record, static layout, no timestep", + "source": "src/gp4/asm/0041/0041724d.cpp:58" + }, + { + "name": "Highest wheel index", + "value": 3, + "unit": "0-based wheel index (four wheels); per wheel per update, rate unverified", + "source": "src/gp4/asm/0041/0041724d.cpp:58" + }, + { + "name": "Side mirroring index bit", + "value": 1, + "unit": "wheel index bit that marks the right-hand side; per wheel per update", + "source": "src/gp4/asm/0041/0041724d.cpp:69" + }, + { + "name": "Angular slip clamp", + "value": "0x006f2afc", + "unit": "address of the double slip limit; per wheel per update, rate unverified", + "source": "src/gp4/asm/0041/0041724d.cpp:61" + }, + { + "name": "Slide warning bit", + "value": "0x80", + "unit": "bit 7 of the wheel status byte; per wheel per update, rate unverified", + "source": "src/gp4/asm/0041/0041724d.cpp:153" + } + ], + "sources": [ + { + "path": "src/gp4/asm/0041/0041724d.cpp", + "line": 58 + }, + { + "path": "src/gp4/asm/0041/0041724d.cpp", + "line": 85 + }, + { + "path": "src/gp4/asm/0041/0041724d.cpp", + "line": 121 + } + ], + "capabilities": [ + "per-wheel tyre load", + "slip clamping", + "combined slip forces", + "slide detection" + ], + "tags": [ + "physics", + "tyres", + "handling" + ] + }, + { + "id": "tyre-slip-curve", + "node": "tyres", + "name": "Soft-saturating slip curve with per-compound records", + "summary": "Slip becomes grip through a double arctangent that rounds off the peak instead of clipping it, with separate front and rear coefficient sets and an activation gate per record.", + "behavior": "The kernel that every tyre force passes through is tiny and shared. The input slip is scaled, blended toward its own arctangent so the curve is flatter near zero without a hard corner, passed through an arctangent again, multiplied by a gain that sets where the peak sits, and finally replaced by the sine of that angle, which is what makes the force fall away again as the slip keeps growing. The angle itself is kept in a global so the rest of the car can read how far the tyre was taken. The coefficients are held in fixed-size records indexed by the wheel slot, and two further coefficient blocks are chosen by axle: the front pair of wheels uses one block and the rear pair another, which is how the setup separates front and rear grip. Each record also carries an activation gate: a scaled value read from the record is compared with a small negative threshold, and while it stays at or above that threshold the curve contributes exactly zero and the blend factor is left untouched, so a wheel whose record is gated off produces no force at all instead of a scaled-down one. The two combined-force routines multiply this kernel result by a shape made of cosines of arctangents of the two slip inputs and their cross terms, which is why a tyre that is already sliding sideways loses longitudinal grip.", + "constants": [ + { + "name": "Curve activation threshold", + "value": -2, + "unit": "signed integer threshold on a scaled record value, the curve stays inactive at or above it; per wheel per update, rate unverified", + "source": "src/gp4/c/0041/00411cc4.cpp:70" + }, + { + "name": "Compound record stride", + "value": "0x158", + "unit": "bytes per compound record, static layout, no timestep", + "source": "src/gp4/c/0041/00411cc4.cpp:59" + }, + { + "name": "Front axle coefficient table", + "value": "0x0062f5e0", + "unit": "address of the front axle coefficient block; per wheel per update, rate unverified", + "source": "src/gp4/c/0041/00411e69.cpp:26" + }, + { + "name": "Rear axle coefficient table", + "value": "0x0062f808", + "unit": "address of the rear axle coefficient block; per wheel per update, rate unverified", + "source": "src/gp4/c/0041/00411e69.cpp:27" + } + ], + "sources": [ + { + "path": "src/gp4/asm/0041/00411e17.cpp", + "line": 11 + }, + { + "path": "src/gp4/c/0041/00411e69.cpp", + "line": 26 + }, + { + "path": "src/gp4/c/0041/00411f09.cpp", + "line": 26 + } + ], + "capabilities": [ + "slip curve shape", + "front and rear grip split", + "activation gate", + "combined slip falloff" + ], + "tags": [ + "physics", + "tyres", + "feel" + ] + }, + { + "id": "aero-downforce-split", + "node": "aero", + "name": "Aerodynamic load blended into the four wheel loads", + "summary": "A stored pair of aerodynamic values is rescaled by a ride-height weight and then halved into the front and rear axle loads, with a per-wheel trim added on top.", + "behavior": "Two passes run every frame. The first reads the current aerodynamic pair and rescales it by a weight that falls from full scale to zero as a height-like level rises above a floor: the level is the magnitude of the mean of one field of the two rear wheel records, the excess over the floor is scaled and clamped so the weight never leaves the fixed-point range, and the pair is multiplied by that weight with a fixed-point shift. Several guards skip the blend entirely, in which case the pair keeps its previous value: a gate value of zero, a magnitude below a threshold, two other rear-record fields disagreeing in sign, or a level still below its floor. The second pass spreads the result into the four wheel records: one wheel share is a per-wheel gain times a shared term, divided by the fixed-point unit, plus half of the front value for the two front wheels and half of the rear value for the two rear wheels, negated depending on the sign of a per-wheel field, which is how the unloaded side of the car is handled. A separate speed-like term, scaled with its own shift and gated by flags that mark a replay and a disabled driver aid, is added to all four wheels with a different gain per axle, which is what gives the car its high-speed platform change.", + "constants": [ + { + "name": "Downforce weight full scale", + "value": "0x4000", + "unit": "Q14 fixed point where 1.0 is this value; per frame, rate unverified", + "source": "src/gp4/c/0041/00416dec.cpp:80" + }, + { + "name": "Front axle downforce value", + "value": "0x0062fba8", + "unit": "address of the front axle fixed-point value; per frame, rate unverified", + "source": "src/gp4/c/0041/00416dec.cpp:93" + }, + { + "name": "Rear axle downforce value", + "value": "0x0062fbac", + "unit": "address of the rear axle fixed-point value; per frame, rate unverified", + "source": "src/gp4/c/0041/00417066.cpp:73" + }, + { + "name": "Extra load term shift", + "value": 11, + "unit": "right shift scaling a speed-like term into wheel load; per frame, rate unverified", + "source": "src/gp4/c/0041/00417066.cpp:102" + } + ], + "sources": [ + { + "path": "src/gp4/c/0041/00416dec.cpp", + "line": 32 + }, + { + "path": "src/gp4/c/0041/00417066.cpp", + "line": 39 + }, + { + "path": "src/gp4/c/0041/00417066.cpp", + "line": 58 + } + ], + "capabilities": [ + "downforce", + "axle balance", + "ride-height sensitivity", + "per-wheel load trim" + ], + "tags": [ + "physics", + "aero", + "handling" + ] + }, + { + "id": "suspension-travel", + "node": "suspension", + "name": "Spring travel, damper rate and lateral load transfer", + "summary": "Per wheel, load is divided by a spring rate and a damper rate to give travel and a ride height; the difference between the axle means is then clamped and moved into the body roll term.", + "behavior": "The routine refreshes the four loads and then treats each wheel the same way. The load is shifted up and divided by a per-wheel spring rate to give a compression, and separately shifted by a smaller amount, divided by a per-wheel valve term and shifted again to give a damper contribution in the wheel travel units. The ride height is a per-wheel rest value with the damper term, the compression and a stored offset subtracted and a stored correction added, plus that wheel record trim. Once all four ride heights exist the routine computes the mean of the rear pair minus the mean of the front pair, clamps that difference to a fixed-point limit so a jump or a kerb strike cannot spike the body, shifts it up by a large factor and divides by the sum of two axle constants, and adds the quotient to the body roll accumulator. That single number is what the renderer and the body attitude use, so a car under braking visibly digs at the front and a car on the throttle squats. A second routine does the same per axle: an axle term from the difference of two wheel load fields, a sum with half a payload term and a base offset, and two divisions that produce the axle position and its scaled counterpart.", + "constants": [ + { + "name": "Spring compression shift", + "value": 10, + "unit": "left shift of the load before dividing by the spring rate; per frame, rate unverified", + "source": "src/gp4/c/0041/0041073d.cpp:61" + }, + { + "name": "Damper term shift", + "value": 6, + "unit": "left shift of the load before dividing by the valve term; per frame, rate unverified", + "source": "src/gp4/c/0041/0041073d.cpp:62" + }, + { + "name": "Lateral transfer clamp", + "value": "0x80000", + "unit": "fixed-point load difference limit; per frame, rate unverified", + "source": "src/gp4/c/0041/0041073d.cpp:80" + }, + { + "name": "Ride-height scaling shift", + "value": 5, + "unit": "left shift of the axle sum before scaling; per frame, rate unverified", + "source": "src/gp4/c/0041/0041c145.cpp:52" + } + ], + "sources": [ + { + "path": "src/gp4/c/0041/0041073d.cpp", + "line": 47 + }, + { + "path": "src/gp4/c/0041/0041073d.cpp", + "line": 75 + }, + { + "path": "src/gp4/c/0041/0041c145.cpp", + "line": 45 + } + ], + "capabilities": [ + "spring compression", + "damping", + "ride height", + "lateral load transfer" + ], + "tags": [ + "physics", + "suspension", + "feel" + ] + }, + { + "id": "steering-heading-response", + "node": "steering", + "name": "Steering value from heading error, with rate limiting", + "summary": "The steering word for the frame comes from the difference between the car heading and the track heading, is clamped, rate-limited toward the target, and then mapped through a response curve.", + "behavior": "Two inputs decide the steering word. When the car has a target close enough and roughly ahead, the bearing to it is used and a lock flag is raised; otherwise the heading of the car against the segment heading decides, with a small tolerance window and a fallback angle for the cars that are spun or reversing. The difference goes through the cosine table in the fixed-point angle representation, keeps the sign of the difference, is clamped to a limit, and raises the left or the right saturation state bit once its magnitude passes a small threshold, so the input layer and the AI can both see how much lock is in use. On top of that sits a rate limiter: the target value minus the current value is limited to a rate picked by the sign and by the state bits, the rate is applied through the frame span, and the state bits are cleared when the value reaches its stop, which is what stops a keyboard tap from producing an instant full-lock step at the front wheels. A separate response routine maps the raw input through a response curve and, in one mode, substitutes a rounded value taken from a per-car table for that curve result, so the same key press can be shaped differently per car.", + "constants": [ + { + "name": "Target lock heading window", + "value": "0x4000", + "unit": "fixed-point heading units (a quarter turn); per frame, rate unverified", + "source": "src/gp4/asm/0041/0041416e.cpp:43" + }, + { + "name": "Saturation threshold on steering magnitude", + "value": "0x80", + "unit": "fixed-point steering magnitude; per frame, rate unverified", + "source": "src/gp4/asm/0041/0041416e.cpp:48" + }, + { + "name": "Rate limiter span shift", + "value": 16, + "unit": "right shift applying the frame span to the target rate; per frame, rate unverified", + "source": "src/gp4/asm/0041/00415b00.cpp:19" + }, + { + "name": "Scaled steering ceiling", + "value": "0x4000", + "unit": "Q14 fixed-point ceiling of the stored scaled steering; per frame, rate unverified", + "source": "src/gp4/asm/0041/00415b00.cpp:22" + } + ], + "sources": [ + { + "path": "src/gp4/asm/0041/0041416e.cpp", + "line": 41 + }, + { + "path": "src/gp4/asm/0041/00415b00.cpp", + "line": 18 + }, + { + "path": "src/gp4/asm/0041/0041b3d9.cpp", + "line": 37 + } + ], + "capabilities": [ + "heading error steering", + "steering rate limit", + "input response curve", + "saturation reporting" + ], + "tags": [ + "controls", + "handling", + "feel" + ] + }, + { + "id": "engine-torque-curve", + "node": "drivetrain", + "name": "Interpolated engine torque curve", + "summary": "A scaled demand plus a per-car level selects a curve position, which is stretched above a knee and read out of a breakpoint table by linear interpolation.", + "behavior": "The torque for one car is evaluated from two curves and a breakpoint table. The curve position is one byte of a packed dword on the car, chosen by the caller, so a single routine serves several torque demands (drive, engine braking, and the same curves read at a different level). The position is combined with the incoming scaled demand, then with a per-level word and a large shift, giving a value in curve-position units. Above a knee the value is stretched by two per-level table entries, which is how the curve keeps rising past its last breakpoint instead of going flat. The stretched position is then looked up in a table of breakpoint pairs terminated by a sentinel: the first pair whose start exceeds the position bounds the segment, the rise between the two breakpoints is divided by the horizontal span with integer division, and the result is added to the base value of the segment. A per-curve floor is enforced afterwards, so the interpolation can never return less than the minimum of the curve, and a per-car level correction is added, or replaces the interpolated value outright in one session mode. Because the arithmetic is integer and fixed-point, the curve is identical on every run, which matters for replays and for the AI cars that use the same routine.", + "constants": [ + { + "name": "Curve position byte selector mask", + "value": 31, + "unit": "shift mask selecting one of four bytes of the packed curve position; per evaluation, rate unverified", + "source": "src/gp4/asm/0044/0044192c.cpp:64" + }, + { + "name": "Curve stretch knee", + "value": "0x280", + "unit": "curve position units, above which the value is stretched; per evaluation, rate unverified", + "source": "src/gp4/asm/0044/0044192c.cpp:76" + }, + { + "name": "Breakpoint pair stride", + "value": 2, + "unit": "table entries per breakpoint pair, static layout, no timestep", + "source": "src/gp4/asm/0044/0044192c.cpp:93" + }, + { + "name": "Breakpoint table terminator", + "value": "0xffffffff", + "unit": "sentinel value ending the breakpoint table, no timestep", + "source": "src/gp4/asm/0044/0044192c.cpp:94" + } + ], + "sources": [ + { + "path": "src/gp4/asm/0044/0044192c.cpp", + "line": 44 + }, + { + "path": "src/gp4/asm/0044/0044192c.cpp", + "line": 72 + }, + { + "path": "src/gp4/asm/0044/0044192c.cpp", + "line": 105 + } + ], + "capabilities": [ + "engine torque curve", + "linear interpolation", + "past-peak stretch", + "per-car levels" + ], + "tags": [ + "drivetrain", + "physics", + "ai" + ] + }, + { + "id": "gearbox-ratio", + "node": "drivetrain", + "name": "Gear ratio scaling and shift softening", + "summary": "A gear byte indexes a ratio table, the ratio scales a difference of two wheel-record fields, and a percentage byte blends the new value with the old across a shift.", + "behavior": "The gearbox side of the car update starts from the gear byte, which is copied to a shadow field so other systems can see the gear actually in use, and looks the ratio up in a per-car byte table. The ratio multiplies a quantity built from two fields of the wheel records, half their sum plus a stored term, taken as a magnitude, and the product is shifted down into the value the drivetrain integrates; that single number is the coupling between the contact patch state and the drivetrain. A separate routine converts the seven-byte gear block in place on a gear change and ramps it over several frames, and a sliding percentage byte blends two torque values, the new one and a held one, with the complement applied in fixed point, which rounds a downshift into a short soft period instead of a step. A per-gear table, indexed by the current gear with a mask and a cap, decides whether the ramp applies at all, and a session flag disables the whole ramp during replay.", + "constants": [ + { + "name": "Gear ratio scaling shift", + "value": 22, + "unit": "right shift converting the scaled product to the stored drivetrain value; per frame, rate unverified", + "source": "src/gp4/asm/0041/00416487.cpp:39" + }, + { + "name": "Blend percentage denominator", + "value": 100, + "unit": "percent, integer; per blending call within an update, rate unverified", + "source": "src/gp4/asm/0041/0041362a.cpp:44" + }, + { + "name": "Gear table index mask", + "value": "0x3f", + "unit": "index mask into the per-gear byte table, static layout, no timestep", + "source": "src/gp4/asm/0041/0041034e.cpp:10" + }, + { + "name": "Gear table index cap", + "value": 10, + "unit": "highest usable per-gear table index, static, no timestep", + "source": "src/gp4/asm/0041/0041034e.cpp:10" + } + ], + "sources": [ + { + "path": "src/gp4/asm/0041/00416487.cpp", + "line": 22 + }, + { + "path": "src/gp4/asm/0041/0041362a.cpp", + "line": 34 + }, + { + "path": "src/gp4/asm/0041/0041034e.cpp", + "line": 4 + } + ], + "capabilities": [ + "gear ratio lookup", + "load-driven engine speed", + "shift blending", + "gear change ramp" + ], + "tags": [ + "drivetrain", + "physics" + ] + }, + { + "id": "collision-sweep", + "node": "collision", + "name": "Swept car-to-car collision over the frame", + "summary": "Two cars are stepped backwards in time in equal increments until their boxes touch, then the contact is refined over four halvings, so a fast closing pair cannot tunnel through each other.", + "behavior": "The two cars are copied into a pair of scratch collision objects together with their headings, and the deltas for one step are the stored velocities scaled by the frame rate plus the difference in headings. If the largest delta is bigger than a fixed threshold the step is halved repeatedly and the fraction is halved with it, which keeps the sample spacing small enough for the box test to catch the contact; the heading is halved with the position so a spinning car cannot slip a corner past the test. Stepping then goes back in time from the end of the frame while the pair does not overlap, up to a step budget, and a pair that never touches reports no contact and leaves the cars alone. When the backward walk does find an overlap, the walk continues until the two are apart, or, when the budget runs out, the pair is pushed apart along the line of their centres instead. The last part is a binary refinement: four halvings alternately step forward and backward depending on whether the boxes overlap, which converges on the touch instant within one step fraction. The contact words are published for the player car and the corrected positions and headings are written back, so the frame never ends with the two cars interpenetrating.", + "constants": [ + { + "name": "Sweep fraction full scale", + "value": "0x4000", + "unit": "Q14 fixed-point 1.0 for the remaining time fraction; per frame, rate unverified", + "source": "src/gp4/asm/0043/00436970.cpp:146" + }, + { + "name": "Step halving threshold", + "value": "0x8000", + "unit": "largest per-step delta allowed before the step is halved; per frame, rate unverified", + "source": "src/gp4/asm/0043/00436970.cpp:147" + }, + { + "name": "Contact refinement passes", + "value": 4, + "unit": "halving passes after the first overlap; per contact, rate unverified", + "source": "src/gp4/asm/0043/00436970.cpp:204" + } + ], + "sources": [ + { + "path": "src/gp4/asm/0043/00436970.cpp", + "line": 102 + }, + { + "path": "src/gp4/asm/0043/00436970.cpp", + "line": 192 + }, + { + "path": "src/gp4/asm/0043/00436970.cpp", + "line": 213 + } + ], + "capabilities": [ + "swept collision", + "time stepping", + "binary contact refinement", + "contact reporting" + ], + "tags": [ + "collision", + "physics", + "feel" + ] + }, + { + "id": "collision-box-contact", + "node": "collision", + "name": "Oriented box contact test", + "summary": "Two oriented boxes are tested by bringing one into the other frame, comparing half extents along three axes and then clipping the eight corners of one box against the other.", + "behavior": "The test first decides which of the two objects is the reference: the one with the larger maximum half extent, with a fixed tie rule, so that the result does not depend on the order the pair arrives in. Both rotation bodies are built and the centres are rotated into world space, then the second box centre is rotated into the first box frame. A quick rejection follows: when the centre offset along any of the three axes exceeds the first box half extent plus the second box radius, the boxes cannot touch and the routine returns clean. Otherwise the second box three half-extent axes are carried into the first frame, which gives a full oriented-box overlap test for the case where one centre is inside the other. The general case takes the eight corners of the second box, forms a Cohen-Sutherland style outcode against the six planes of the first box, and rejects when all corners share the same outside bit. A corner inside, or one of the twelve edges that survives clipping, is a contact. The flags of the routine carry the boolean, so the caller can branch without a second call.", + "constants": [ + { + "name": "Box corner count", + "value": 8, + "unit": "corners per oriented box; per contact test, rate unverified", + "source": "src/gp4/asm/0043/00438c98.cpp:89" + }, + { + "name": "Collision shape block stride", + "value": "0x5d8", + "unit": "bytes per shape block, static layout, no timestep", + "source": "src/gp4/asm/0043/00438c98.cpp:83" + }, + { + "name": "Clipped edge count", + "value": 12, + "unit": "box edges tested after the corner rejection; per contact test, rate unverified", + "source": "src/gp4/asm/0043/00438c98.cpp:91" + } + ], + "sources": [ + { + "path": "src/gp4/asm/0043/00438c98.cpp", + "line": 82 + }, + { + "path": "src/gp4/asm/0043/00438c98.cpp", + "line": 87 + }, + { + "path": "src/gp4/asm/0043/00438c98.cpp", + "line": 94 + } + ], + "capabilities": [ + "oriented box overlap", + "outcode clipping", + "reference selection", + "boolean contact flag" + ], + "tags": [ + "collision", + "physics" + ] + }, + { + "id": "collision-response", + "node": "collision", + "name": "Mass-weighted separation of two touching bodies", + "summary": "The heavier body moves less: a fixed-point mass fraction weights how far each body is pushed out of the other, and the contact flags are cleared.", + "behavior": "When two bodies are found to touch, the response is a positional one rather than an impulse. The mass of the first body is divided by the sum of both masses in fixed point, with the degenerate zero-mass case guarded by a denominator of one, and the complement is formed as the remainder of the full scale. The two coordinates that describe the contact are then blended toward each other, each body moving by its own share of the overlap, with the pairs of coordinates updated so that two wheels touching a wall or an opponent separate over the following frames instead of snapping. The share of the lighter body is larger, so a car that hits a much heavier one is the one that is deflected, which is the behaviour players read as an opponent being immovable. The last step clears the two contact bytes so the pair is not resolved twice in the same frame, and the intermediate values are left in registers for the caller, which uses them for the contact effect and the sound trigger.", + "constants": [ + { + "name": "Mass fraction fixed-point shift", + "value": 14, + "unit": "left shift forming the Q14 mass fraction; per contact, rate unverified", + "source": "src/gp4/asm/0043/00439f75.cpp:77" + }, + { + "name": "Mass fraction full scale", + "value": "0x4000", + "unit": "Q14 fixed-point 1.0 for the complementary weight; per contact, rate unverified", + "source": "src/gp4/asm/0043/00439f75.cpp:79" + }, + { + "name": "Separation stiffness", + "value": "0x006097f8", + "unit": "address of the 32-bit spring factor used to blend the coordinates; per contact, rate unverified", + "source": "src/gp4/asm/0043/00439f75.cpp:80" + } + ], + "sources": [ + { + "path": "src/gp4/asm/0043/00439f75.cpp", + "line": 63 + }, + { + "path": "src/gp4/asm/0043/00439f75.cpp", + "line": 74 + }, + { + "path": "src/gp4/asm/0043/00439f75.cpp", + "line": 85 + } + ], + "capabilities": [ + "mass-weighted separation", + "positional contact response", + "contact flag clearing" + ], + "tags": [ + "collision", + "physics", + "feel" + ] + }, + { + "id": "track-lap-position", + "node": "track-ring", + "name": "Lap position from the segment ring", + "summary": "A car position is the distance along the lap: the segment index and the projection inside the segment are summed, offset by the start line and wrapped into the lap length.", + "behavior": "The track is a circular list of fixed-size segment records with the pit lane and the racing lap held as separate ranges in the same array, which is why the walkers have two wrap rules. Forward and backward stepping each advance by one record, and near the pit entry the pit lane clamps the walk while below the lap start the walk wraps around to the other end of the lap ring, so a car cannot walk backwards out of the pit lane into the previous lap. The lap position itself comes from the segment index, masked to its index bits, plus a conversion term applied when the car carries a particular state flag, reduced and wrapped by the lap length and offset by the start-line word, giving a value that only ever lies between zero and the lap length. Two scaled copies are stored on the car, one of them with the sub-segment fraction, and a further routine fills the lateral lane words used by the pit and AI logic. This one value drives the standings, the progress comparison between two cars and the DRS-style gap logic, so it has to be monotone along the lap and identical for every car on every frame.", + "constants": [ + { + "name": "Segment index mask", + "value": "0x7ff", + "unit": "index bits of the segment number, static layout, no timestep", + "source": "src/gp4/asm/0040/00409467.cpp:19" + }, + { + "name": "Segment record stride", + "value": "0x160", + "unit": "bytes per track segment record, static layout, no timestep", + "source": "src/gp4/asm/0041/0041cb9b.cpp:38" + }, + { + "name": "Lap length value", + "value": "0x00868558", + "unit": "address of the 16-bit lap length in track units; per frame, rate unverified", + "source": "src/gp4/asm/0040/00409467.cpp:41" + } + ], + "sources": [ + { + "path": "src/gp4/asm/0040/00409467.cpp", + "line": 4 + }, + { + "path": "src/gp4/asm/0041/0041cb9b.cpp", + "line": 35 + }, + { + "path": "src/gp4/asm/0040/00409467.cpp", + "line": 35 + } + ], + "capabilities": [ + "lap position", + "segment ring walking", + "pit and lap wrap rules", + "progress comparison" + ], + "tags": [ + "track", + "race rules", + "timing" + ] + }, + { + "id": "race-clock-and-digit-split", + "node": "race-clock", + "name": "Race clock, remaining time and digit split", + "summary": "Two 48-bit fixed-point clocks advance by per-frame deltas; the remaining session time is clamped at zero and split into decimal digits for the timing display.", + "behavior": "The session time is kept as two fixed-point accumulators, each a 32-bit whole part with a 16-bit fraction, advanced every frame by a delta that the session code sets and carried on overflow. One accumulator is the wall clock of the session and the other is the timer whose remaining value is shown to the driver. Remaining time is the configured limit minus the clock, floored at zero, and it is converted by a routine that packs the decimal digits of a millisecond value into one digit per nibble by repeated subtraction at the thousand, ten thousand, minute, ten minute and hour thresholds; the packed result is shifted down before it is stored, so the display reads whole units. On a new value a change flag is raised so the front end only redraws the digits when they actually move. The same pass arms and then fires the session countdown: when the session flags ask for it, a deadline is set a few thousand clock units ahead, and once the clock passes it the corresponding flags are raised so the session code can react. A 24-hour deadline is armed in milliseconds for the day-long sessions, and the selected car refreshes its own lap digits and raises a flag when its lap-time value changes.", + "constants": [ + { + "name": "Day deadline", + "value": 86400000, + "unit": "milliseconds in 24 hours; static session limit, no timestep", + "source": "src/gp4/c/0044/00442cb4.cpp:22" + }, + { + "name": "Countdown deadline span", + "value": 6000, + "unit": "clock units added to the current clock when the countdown arms; per frame, rate unverified", + "source": "src/gp4/c/0044/00443393.cpp:23" + }, + { + "name": "Minute digit threshold", + "value": 60000, + "unit": "milliseconds per packed minute digit; static, no timestep", + "source": "src/gp4/asm/0040/0040996f.cpp:43" + }, + { + "name": "Second digit threshold", + "value": 1000, + "unit": "milliseconds per packed second digit; static, no timestep", + "source": "src/gp4/asm/0040/0040996f.cpp:45" + } + ], + "sources": [ + { + "path": "src/gp4/c/0044/00443393.cpp", + "line": 18 + }, + { + "path": "src/gp4/asm/0040/0040996f.cpp", + "line": 24 + }, + { + "path": "src/gp4/c/0044/00442cb4.cpp", + "line": 22 + } + ], + "capabilities": [ + "session clock", + "remaining time", + "digit packing", + "countdown", + "lap time refresh" + ], + "tags": [ + "race rules", + "timing", + "hud" + ] + }, + { + "id": "ai-line-following", + "node": "ai-driver", + "name": "AI target point, frame damping and heading turn", + "summary": "An AI car takes the target point ahead, damps its offsets and closing speed in that point frame, and turns its heading toward the target with a deadband and a rate cap.", + "behavior": "The AI car first decides whether the driver logic should run at all, which it should not in the pit lane, when the car is flagged as being under manual control, or when the state byte that marks a scripted car is clear. It then asks for the target point ahead on the racing line for the segment the car is on, translates the car position into that target frame, and rotates the closing speed components into the same frame with a sine and cosine pair built from the segment heading. That rotation is what makes the following behaviour independent of the world orientation: the same numbers work on every corner of every track. An offset damping pass then pulls the lateral and longitudinal offsets toward the target with a share taken from the target frame, and the results are rotated back. The heading turn is computed from the difference between the car heading and the bearing to the target: a deadband is subtracted from the magnitude first, so the AI does not saw at the wheel down a straight, and the excess is scaled, multiplied by the frame span, shifted down and capped to a maximum turn per frame before it is added to the heading. A companion routine handles the overtaking case, steering for the gap between a lane edge and the car ahead and adjusting the steering word when the space on the inside is smaller than a threshold.", + "constants": [ + { + "name": "Velocity frame scaling shift", + "value": 8, + "unit": "right shift converting the stored velocity into the target frame; per AI update, rate unverified", + "source": "src/gp4/asm/0043/00433709.cpp:120" + }, + { + "name": "Half-turn heading offset", + "value": "0x80000000", + "unit": "heading units, a half turn of the 32-bit heading circle; per AI update, rate unverified", + "source": "src/gp4/asm/0043/00433709.cpp:112" + }, + { + "name": "Heading deadband", + "value": "0x00601be8", + "unit": "address of the deadband in heading units; per AI update, rate unverified", + "source": "src/gp4/asm/0043/00433709.cpp:276" + }, + { + "name": "Turn integration shift", + "value": 16, + "unit": "right shift applying the frame span to the turn rate; per AI update, rate unverified", + "source": "src/gp4/asm/0043/00433709.cpp:288" + }, + { + "name": "Inside gap threshold", + "value": "0x240", + "unit": "track lateral units below which the overtake steering is adjusted; per AI update, rate unverified", + "source": "src/gp4/asm/0042/0042c1f4.cpp:91" + } + ], + "sources": [ + { + "path": "src/gp4/asm/0043/00433709.cpp", + "line": 101 + }, + { + "path": "src/gp4/asm/0043/00433709.cpp", + "line": 260 + }, + { + "path": "src/gp4/asm/0042/0042c1f4.cpp", + "line": 89 + } + ], + "capabilities": [ + "target point following", + "local frame damping", + "heading control with deadband", + "overtake gap logic" + ], + "tags": [ + "ai", + "driving", + "handling" + ] + }, + { + "id": "pit-lane-detection", + "node": "pit-lane", + "name": "Pit lane membership and lateral limit", + "summary": "Each car and frame, the pit logic decides whether the car is on a lane, which kind of lane, and how far it may drift laterally before the lane entry is refused.", + "behavior": "The per-car pit state is decided from a few flag bits, the session byte that says whether a pit lane exists at all, and the distance between the car segment and the segment the pit logic currently holds. A car that is not on a lane has its lane kind cleared along with the bits that mark an approach and a committed entry, so the state cannot leak from the previous lap. A car that is on one takes a lane kind of one to five taken from its own flags, and the bits that mark the lane phase are set from those flags; the comparison of a signed lateral word against a stored offset then picks between the inside and the outside half of the lane. A separate routine produces the lateral limit that lane entry allows: it reads a fixed-size lane entry from the lane table indexed by the segment, clamps the bound, eases it toward the stored weight of the car when the car is already part of the queue, refines it with an extra pass and a shift, and returns the signed lateral room as the product of the value and the mean of the value and the reference. That limit is what stops an AI car from cutting across the pit entry line and what lets the player be steered back onto the lane without a hard wall.", + "constants": [ + { + "name": "Lane kind code top", + "value": 5, + "unit": "highest lane kind code (codes 1 to 5); per car per frame, rate unverified", + "source": "src/gp4/asm/0043/004320f2.cpp:45" + }, + { + "name": "Lane blend shift", + "value": 2, + "unit": "left shift on the blend weight before the 16-bit product; per lane evaluation, rate unverified", + "source": "src/gp4/asm/0042/00425cb3.cpp:30" + }, + { + "name": "Lane table entry size", + "value": 16, + "unit": "bytes per lane table entry, static layout, no timestep", + "source": "src/gp4/asm/0042/00425cb3.cpp:42" + } + ], + "sources": [ + { + "path": "src/gp4/asm/0043/004320f2.cpp", + "line": 42 + }, + { + "path": "src/gp4/asm/0042/00425cb3.cpp", + "line": 42 + }, + { + "path": "src/gp4/asm/0042/00425cb3.cpp", + "line": 46 + } + ], + "capabilities": [ + "pit lane membership", + "lane kind state", + "lateral lane limit", + "pit entry assist" + ], + "tags": [ + "race rules", + "track", + "ai" + ] + }, + { + "id": "camera-view-matrices", + "node": "camera", + "name": "Perspective and overhead view matrices", + "summary": "The projection is built from a field of view, a square aspect and long clipping range, and the view is placed at a raised point looking straight down at the car with an up vector flattened onto the ground plane.", + "behavior": "The view build is a small deterministic pair. The projection is a standard perspective with its vertical field of view derived as twice an arctangent of a stored tangent, an aspect of one, a near plane of one and a far plane far out in world units, which is why the game can show both a car three metres away and a straight half a kilometre long without popping. The view puts the eye directly above the car position by a fixed vertical offset and looks straight down at the car, so the picture stays locked to the car whatever the track orientation; the up direction is the body forward axis, transformed as a normal and flattened onto the ground plane, then scaled to a fixed length so the roll of the picture never degenerates when the car points straight along an axis. Both matrices are identity-initialised before they are filled, so the homogeneous row is well defined even if a routine leaves it untouched. A second routine derives the camera body basis itself: three cross-product-like terms from the body vectors, a square root of the squared length and, when that length exceeds a stored limit, a uniform rescale, which keeps the basis orthonormal after the many small additions the frame makes to it.", + "constants": [ + { + "name": "Far clipping plane", + "value": 100000, + "unit": "world units; rebuilt per camera update, rate unverified", + "source": "src/gp4/c/0049/00492c10.cpp:36" + }, + { + "name": "Near clipping plane", + "value": 1, + "unit": "world units; rebuilt per camera update, rate unverified", + "source": "src/gp4/c/0049/00492c10.cpp:36" + }, + { + "name": "Projection aspect", + "value": 1, + "unit": "width divided by height, dimensionless; rebuilt per camera update, rate unverified", + "source": "src/gp4/c/0049/00492c10.cpp:36" + }, + { + "name": "Camera basis length clamp", + "value": "0x00609758", + "unit": "address of the 32-bit maximum basis length; per frame, rate unverified", + "source": "src/gp4/c/0040/004024b5.cpp:25" + } + ], + "sources": [ + { + "path": "src/gp4/c/0049/00492c10.cpp", + "line": 15 + }, + { + "path": "src/gp4/c/0040/0040233e.cpp", + "line": 5 + }, + { + "path": "src/gp4/c/0040/004024b5.cpp", + "line": 12 + } + ], + "capabilities": [ + "perspective projection", + "overhead view matrix", + "orthonormal camera basis", + "up vector flattening" + ], + "tags": [ + "camera", + "rendering", + "3d" + ] + } + ] +} diff --git a/data/games/lego-racers.json b/data/games/lego-racers.json new file mode 100644 index 0000000..a6a78b5 --- /dev/null +++ b/data/games/lego-racers.json @@ -0,0 +1,3065 @@ +{ + "version": 1, + "id": "lego-racers", + "title": "LEGO Racers", + "repo": "https://github.com/isledecomp/racers", + "sha": "f1530e636f7166c5d3cdd0b7cbadac683741d0ce", + "nodes": [ + { + "id": "rigid-body", + "name": "Rigid body core", + "purpose": "Force, torque and impulse bookkeeping for a box-shaped chassis, plus the integration step that turns accumulated force into position and orientation", + "paths": [ + "LEGORacers/src/race/racer/racerrigidbody.cpp", + "LEGORacers/include/race/racer/racerrigidbody.h", + "LEGORacers/include/race/racer/racerboxbody.h" + ], + "functions": [ + { + "name": "RacerRigidBody::Update", + "path": "LEGORacers/src/race/racer/racerrigidbody.cpp", + "lines": [ + 256, + 386 + ] + }, + { + "name": "RacerCarBody::AddForce", + "path": "LEGORacers/src/race/racer/racerrigidbody.cpp", + "lines": [ + 105, + 112 + ] + }, + { + "name": "RacerCarBody::AddForceAtPoint", + "path": "LEGORacers/src/race/racer/racerrigidbody.cpp", + "lines": [ + 113, + 145 + ] + }, + { + "name": "RacerCarBody::AddAngularImpulse", + "path": "LEGORacers/src/race/racer/racerrigidbody.cpp", + "lines": [ + 146, + 186 + ] + }, + { + "name": "RacerCarBody::CancelAngularMomentumAlong", + "path": "LEGORacers/src/race/racer/racerrigidbody.cpp", + "lines": [ + 239, + 255 + ] + }, + { + "name": "RacerRigidBody::UpdateAngularVelocity", + "path": "LEGORacers/src/race/racer/racerrigidbody.cpp", + "lines": [ + 89, + 104 + ] + }, + { + "name": "RacerBoxBody::ComputeInertiaTensor", + "path": "LEGORacers/src/race/racer/racerrigidbody.cpp", + "lines": [ + 427, + 462 + ] + } + ], + "dependsOn": [] + }, + { + "id": "car-body", + "parent": "rigid-body", + "name": "Car body and wheel contact", + "purpose": "The arcade vehicle model: force accumulation, four wheel probes against track geometry, ground snapping, wall response, upright clamping, spin reactions and thrust/drag", + "paths": [ + "LEGORacers/src/race/racer/racercarbody.cpp", + "LEGORacers/include/race/racer/racercarbody.h" + ], + "functions": [ + { + "name": "RacerCarBody::Update", + "path": "LEGORacers/src/race/racer/racercarbody.cpp", + "lines": [ + 294, + 481 + ] + }, + { + "name": "RacerCarBody::AccumulateForces", + "path": "LEGORacers/src/race/racer/racercarbody.cpp", + "lines": [ + 562, + 818 + ] + }, + { + "name": "RacerCarBody::UpdateWheelContacts", + "path": "LEGORacers/src/race/racer/racercarbody.cpp", + "lines": [ + 903, + 1153 + ] + }, + { + "name": "RacerCarBody::UpdateSlideContacts", + "path": "LEGORacers/src/race/racer/racercarbody.cpp", + "lines": [ + 1192, + 1487 + ] + }, + { + "name": "RacerCarBody::ResolveWallCollisions", + "path": "LEGORacers/src/race/racer/racercarbody.cpp", + "lines": [ + 1884, + 2029 + ] + }, + { + "name": "RacerCarBody::ApplyWallResponse", + "path": "LEGORacers/src/race/racer/racercarbody.cpp", + "lines": [ + 2030, + 2129 + ] + }, + { + "name": "RacerCarBody::LimitUprightTilt", + "path": "LEGORacers/src/race/racer/racercarbody.cpp", + "lines": [ + 878, + 902 + ] + }, + { + "name": "RacerCarBody::SnapToContacts", + "path": "LEGORacers/src/race/racer/racercarbody.cpp", + "lines": [ + 2339, + 2418 + ] + }, + { + "name": "RacerCarBody::UpdateDragCoefficient", + "path": "LEGORacers/src/race/racer/racercarbody.cpp", + "lines": [ + 2322, + 2338 + ] + }, + { + "name": "RacerCarBody::UpdateFacingDirection", + "path": "LEGORacers/src/race/racer/racercarbody.cpp", + "lines": [ + 1544, + 1649 + ] + }, + { + "name": "RacerCarBody::StartSpin", + "path": "LEGORacers/src/race/racer/racercarbody.cpp", + "lines": [ + 2227, + 2269 + ] + }, + { + "name": "RacerCarBody::StartSpinOut", + "path": "LEGORacers/src/race/racer/racercarbody.cpp", + "lines": [ + 2505, + 2511 + ] + }, + { + "name": "RacerPhysics::StartPowerslide", + "path": "LEGORacers/src/race/racer/racercarbody.cpp", + "lines": [ + 2878, + 2888 + ] + }, + { + "name": "RacerCarBody::UpdateVisualBank", + "path": "LEGORacers/src/race/racer/racercarbody.cpp", + "lines": [ + 482, + 561 + ] + } + ], + "dependsOn": [ + "rigid-body" + ] + }, + { + "id": "drive-controller", + "name": "Drive controller", + "purpose": "Translates player or scripted input into a signed turn radius and a thrust value, owns the powerslide state machine and the recovery steer-back-to-route behaviour", + "paths": [ + "LEGORacers/src/race/racer/drivecontroller.cpp", + "LEGORacers/include/race/racer/drivecontroller.h" + ], + "functions": [ + { + "name": "DriveController::Update", + "path": "LEGORacers/src/race/racer/drivecontroller.cpp", + "lines": [ + 117, + 174 + ] + }, + { + "name": "DriveController::SetSteeringInput", + "path": "LEGORacers/src/race/racer/drivecontroller.cpp", + "lines": [ + 220, + 239 + ] + }, + { + "name": "DriveController::ApplySteering", + "path": "LEGORacers/src/race/racer/drivecontroller.cpp", + "lines": [ + 240, + 315 + ] + }, + { + "name": "DriveController::SetThrottleInput", + "path": "LEGORacers/src/race/racer/drivecontroller.cpp", + "lines": [ + 318, + 338 + ] + }, + { + "name": "DriveController::ApplyThrust", + "path": "LEGORacers/src/race/racer/drivecontroller.cpp", + "lines": [ + 341, + 369 + ] + }, + { + "name": "DriveController::EngageSlide", + "path": "LEGORacers/src/race/racer/drivecontroller.cpp", + "lines": [ + 372, + 391 + ] + }, + { + "name": "DriveController::ReleaseSlide", + "path": "LEGORacers/src/race/racer/drivecontroller.cpp", + "lines": [ + 392, + 404 + ] + }, + { + "name": "DriveController::UpdateStuckDetection", + "path": "LEGORacers/src/race/racer/drivecontroller.cpp", + "lines": [ + 189, + 219 + ] + }, + { + "name": "DriveController::UpdateReturnToPath", + "path": "LEGORacers/src/race/racer/drivecontroller.cpp", + "lines": [ + 450, + 544 + ] + } + ], + "dependsOn": [ + "car-body" + ] + }, + { + "id": "player-input", + "name": "Player input shaping", + "purpose": "Ramps digital steering into an analogue value, converts throttle and brake into a drive signal, opens the powerslide on the drift button and arms the timed throttle-release boost", + "paths": [ + "LEGORacers/src/race/playercontrols.cpp", + "LEGORacers/include/race/playercontrols.h" + ], + "functions": [ + { + "name": "PlayerControls::UpdateSteering", + "path": "LEGORacers/src/race/playercontrols.cpp", + "lines": [ + 84, + 212 + ] + }, + { + "name": "PlayerControls::UpdateThrottle", + "path": "LEGORacers/src/race/playercontrols.cpp", + "lines": [ + 213, + 288 + ] + }, + { + "name": "PlayerControls::Update", + "path": "LEGORacers/src/race/playercontrols.cpp", + "lines": [ + 289, + 304 + ] + }, + { + "name": "PlayerControls::TryStartBoost", + "path": "LEGORacers/src/race/playercontrols.cpp", + "lines": [ + 418, + 438 + ] + }, + { + "name": "PlayerControls::TrackThrottleHold", + "path": "LEGORacers/src/race/playercontrols.cpp", + "lines": [ + 451, + 478 + ] + } + ], + "dependsOn": [ + "drive-controller" + ] + }, + { + "id": "racer-physics", + "name": "Racer physics and stats", + "purpose": "Wraps the car body with per-racer stats, boost and curse state, per-surface grip recording from wheel hits and the route-following trajectory mode used by path-driven racers", + "paths": [ + "LEGORacers/src/race/racer/racerphysics.cpp", + "LEGORacers/include/race/racer/racerphysics.h" + ], + "functions": [ + { + "name": "RacerPhysics::Reset", + "path": "LEGORacers/src/race/racer/racerphysics.cpp", + "lines": [ + 178, + 215 + ] + }, + { + "name": "RacerPhysics::StartBoost", + "path": "LEGORacers/src/race/racer/racerphysics.cpp", + "lines": [ + 265, + 280 + ] + }, + { + "name": "RacerPhysics::EndBoost", + "path": "LEGORacers/src/race/racer/racerphysics.cpp", + "lines": [ + 283, + 295 + ] + }, + { + "name": "RacerPhysics::SetHandlingStat", + "path": "LEGORacers/src/race/racer/racerphysics.cpp", + "lines": [ + 1336, + 1342 + ] + }, + { + "name": "RacerPhysics::SetAccelerationStat", + "path": "LEGORacers/src/race/racer/racerphysics.cpp", + "lines": [ + 1343, + 1349 + ] + }, + { + "name": "RacerPhysics::SetTopSpeedStat", + "path": "LEGORacers/src/race/racer/racerphysics.cpp", + "lines": [ + 1350, + 1357 + ] + }, + { + "name": "RacerPhysics::UpdateWheelSurfaces", + "path": "LEGORacers/src/race/racer/racerphysics.cpp", + "lines": [ + 1118, + 1196 + ] + }, + { + "name": "RacerPhysics::ApplyWheelSurface", + "path": "LEGORacers/src/race/racer/racerphysics.cpp", + "lines": [ + 1221, + 1300 + ] + }, + { + "name": "RacerPhysics::CanPowerslide", + "path": "LEGORacers/src/race/racer/racerphysics.cpp", + "lines": [ + 1364, + 1396 + ] + }, + { + "name": "RacerPhysics::CanSteer", + "path": "LEGORacers/src/race/racer/racerphysics.cpp", + "lines": [ + 1397, + 1432 + ] + }, + { + "name": "RacerPhysics::StartSpinOut", + "path": "LEGORacers/src/race/racer/racerphysics.cpp", + "lines": [ + 680, + 688 + ] + }, + { + "name": "RacerPhysics::ApplyDirectionalImpulse", + "path": "LEGORacers/src/race/racer/racerphysics.cpp", + "lines": [ + 446, + 502 + ] + } + ], + "dependsOn": [ + "car-body", + "route-motion" + ] + }, + { + "id": "route-motion", + "name": "Route trajectory playback", + "purpose": "Time-parameterised playback along a recorded race route: playback speed ramps, lateral offset, tilt, jump arc and wall fallback for racers that do not drive themselves", + "paths": [ + "LEGORacers/src/race/racer/routecursor.cpp", + "LEGORacers/src/race/racer/racerphysics.cpp", + "LEGORacers/include/race/racer/racerouterecord.h" + ], + "functions": [ + { + "name": "RacerPhysics::UpdateRouteMotion", + "path": "LEGORacers/src/race/racer/racerphysics.cpp", + "lines": [ + 748, + 882 + ] + }, + { + "name": "RacerPhysics::UpdateRouteRotation", + "path": "LEGORacers/src/race/racer/racerphysics.cpp", + "lines": [ + 925, + 1004 + ] + }, + { + "name": "RacerPhysics::ApplyRoutePosition", + "path": "LEGORacers/src/race/racer/racerphysics.cpp", + "lines": [ + 1005, + 1035 + ] + }, + { + "name": "RacerPhysics::AttachRoute", + "path": "LEGORacers/src/race/racer/racerphysics.cpp", + "lines": [ + 1036, + 1052 + ] + }, + { + "name": "RacerPhysics::RouteCursor::Advance", + "path": "LEGORacers/src/race/racer/routecursor.cpp", + "lines": [ + 127, + 296 + ] + }, + { + "name": "RacerPhysics::RouteCursor::SeekByDelta", + "path": "LEGORacers/src/race/racer/routecursor.cpp", + "lines": [ + 297, + 505 + ] + } + ], + "dependsOn": [ + "rigid-body" + ] + }, + { + "id": "race-camera", + "name": "Race camera controller", + "purpose": "Third-person chase, cockpit and finish cameras with per-view profiles, exponential lag smoothing, turn lead, speed-driven distance changes and field-of-view shake", + "paths": [ + "LEGORacers/src/race/racecameracontroller.cpp", + "LEGORacers/include/race/racecameracontroller.h" + ], + "functions": [ + { + "name": "RaceCameraController::Update", + "path": "LEGORacers/src/race/racecameracontroller.cpp", + "lines": [ + 487, + 802 + ] + }, + { + "name": "RaceCameraController::UpdateFollow", + "path": "LEGORacers/src/race/racecameracontroller.cpp", + "lines": [ + 154, + 212 + ] + }, + { + "name": "RaceCameraController::UpdateShake", + "path": "LEGORacers/src/race/racecameracontroller.cpp", + "lines": [ + 223, + 290 + ] + }, + { + "name": "RaceCameraController::ApplySmoothing", + "path": "LEGORacers/src/race/racecameracontroller.cpp", + "lines": [ + 291, + 323 + ] + }, + { + "name": "RaceCameraController::SetView", + "path": "LEGORacers/src/race/racecameracontroller.cpp", + "lines": [ + 440, + 473 + ] + }, + { + "name": "RaceCameraController::Reset", + "path": "LEGORacers/src/race/racecameracontroller.cpp", + "lines": [ + 324, + 344 + ] + }, + { + "name": "RaceCameraController::UpdateListener", + "path": "LEGORacers/src/race/racecameracontroller.cpp", + "lines": [ + 103, + 153 + ] + } + ], + "dependsOn": [ + "car-body" + ] + }, + { + "id": "powerup-bricks", + "name": "Brick pickups and powerup selection", + "purpose": "Coloured pickup bricks, the white bricks that raise a held powerup level and the colour/level matrix that decides which effect fires", + "paths": [ + "LEGORacers/src/race/powerups/pickupbrick.cpp", + "LEGORacers/src/race/powerups/colorbrick.cpp", + "LEGORacers/src/race/powerups/whitebrick.cpp", + "LEGORacers/src/race/powerups/racepowerupmanager.cpp", + "LEGORacers/include/race/racer/racer.h" + ], + "functions": [ + { + "name": "PickupBrick::Update", + "path": "LEGORacers/src/race/powerups/pickupbrick.cpp", + "lines": [ + 95, + 130 + ] + }, + { + "name": "PickupBrick::OnEvent", + "path": "LEGORacers/src/race/powerups/pickupbrick.cpp", + "lines": [ + 142, + 156 + ] + }, + { + "name": "ColorBrick::Update", + "path": "LEGORacers/src/race/powerups/colorbrick.cpp", + "lines": [ + 57, + 109 + ] + }, + { + "name": "ColorBrick::OnTouched", + "path": "LEGORacers/src/race/powerups/colorbrick.cpp", + "lines": [ + 110, + 141 + ] + }, + { + "name": "WhiteBrick::Update", + "path": "LEGORacers/src/race/powerups/whitebrick.cpp", + "lines": [ + 50, + 92 + ] + }, + { + "name": "Racer::CollectWhiteBrick", + "path": "LEGORacers/src/race/racer/racer.cpp", + "lines": [ + 1878, + 1912 + ] + }, + { + "name": "RacePowerupManager::UseRedPowerup", + "path": "LEGORacers/src/race/powerups/racepowerupmanager.cpp", + "lines": [ + 1336, + 1355 + ] + }, + { + "name": "RacePowerupManager::UseYellowPowerup", + "path": "LEGORacers/src/race/powerups/racepowerupmanager.cpp", + "lines": [ + 1357, + 1378 + ] + }, + { + "name": "RacePowerupManager::UseBluePowerup", + "path": "LEGORacers/src/race/powerups/racepowerupmanager.cpp", + "lines": [ + 1680, + 1768 + ] + }, + { + "name": "RacePowerupManager::UseGreenPowerup", + "path": "LEGORacers/src/race/powerups/racepowerupmanager.cpp", + "lines": [ + 1770, + 1791 + ] + } + ], + "dependsOn": [] + }, + { + "id": "powerup-actions", + "name": "Powerup actions", + "purpose": "Timed effect objects: turbo with three levels, shield with four levels, lobbed projectiles with a solved trajectory and homing projectiles", + "paths": [ + "LEGORacers/src/race/powerups/turboaction.cpp", + "LEGORacers/src/race/powerups/shieldaction.cpp", + "LEGORacers/src/race/powerups/powerupprojectile.cpp", + "LEGORacers/src/race/powerups/homingprojectile.cpp", + "LEGORacers/src/race/powerups/cannonballaction.cpp" + ], + "functions": [ + { + "name": "TurboAction::Activate", + "path": "LEGORacers/src/race/powerups/turboaction.cpp", + "lines": [ + 91, + 210 + ] + }, + { + "name": "TurboAction::StartBoost", + "path": "LEGORacers/src/race/powerups/turboaction.cpp", + "lines": [ + 212, + 243 + ] + }, + { + "name": "TurboAction::AdvanceState", + "path": "LEGORacers/src/race/powerups/turboaction.cpp", + "lines": [ + 363, + 434 + ] + }, + { + "name": "ShieldAction::Activate", + "path": "LEGORacers/src/race/powerups/shieldaction.cpp", + "lines": [ + 63, + 180 + ] + }, + { + "name": "ShieldAction::AdvanceState", + "path": "LEGORacers/src/race/powerups/shieldaction.cpp", + "lines": [ + 269, + 299 + ] + }, + { + "name": "PowerupProjectile::LaunchAtPoint", + "path": "LEGORacers/src/race/powerups/powerupprojectile.cpp", + "lines": [ + 180, + 250 + ] + }, + { + "name": "PowerupProjectile::ComputeTrajectory", + "path": "LEGORacers/src/race/powerups/powerupprojectile.cpp", + "lines": [ + 332, + 353 + ] + }, + { + "name": "PowerupProjectile::Update", + "path": "LEGORacers/src/race/powerups/powerupprojectile.cpp", + "lines": [ + 387, + 433 + ] + }, + { + "name": "PowerupProjectile::Deflect", + "path": "LEGORacers/src/race/powerups/powerupprojectile.cpp", + "lines": [ + 289, + 331 + ] + }, + { + "name": "HomingProjectile::UpdateTargeting", + "path": "LEGORacers/src/race/powerups/homingprojectile.cpp", + "lines": [ + 81, + 163 + ] + }, + { + "name": "HomingProjectile::Update", + "path": "LEGORacers/src/race/powerups/homingprojectile.cpp", + "lines": [ + 200, + 330 + ] + }, + { + "name": "HomingProjectile::ApplySpiral", + "path": "LEGORacers/src/race/powerups/homingprojectile.cpp", + "lines": [ + 339, + 366 + ] + } + ], + "dependsOn": [ + "powerup-bricks", + "car-body" + ] + }, + { + "id": "racer-contact", + "name": "Racer versus racer contact", + "purpose": "Impulse solution when two chassis touch, the shield-versus-shield rules that follow it, shove timers and the curse hand-over", + "paths": [ + "LEGORacers/src/race/racer/racer.cpp", + "LEGORacers/src/race/racercollisionworlds.cpp" + ], + "functions": [ + { + "name": "Racer::OnEvent", + "path": "LEGORacers/src/race/racer/racer.cpp", + "lines": [ + 1365, + 1626 + ] + }, + { + "name": "Racer::ApplyShove", + "path": "LEGORacers/src/race/racer/racer.cpp", + "lines": [ + 1627, + 1657 + ] + }, + { + "name": "Racer::StartShield", + "path": "LEGORacers/src/race/racer/racer.cpp", + "lines": [ + 2060, + 2067 + ] + }, + { + "name": "Racer::AttachCurse", + "path": "LEGORacers/src/race/racer/racer.cpp", + "lines": [ + 2153, + 2215 + ] + } + ], + "dependsOn": [ + "car-body", + "powerup-actions" + ] + }, + { + "id": "race-progress", + "name": "Lap and checkpoint progress", + "purpose": "Oriented checkpoint planes that count progress, the three-zone finish sequence that validates a completed lap and the standings comparison that follows it", + "paths": [ + "LEGORacers/src/race/checkpointgraph.cpp", + "LEGORacers/src/race/checkpointgraphwalk.cpp", + "LEGORacers/src/race/events/lapzoneresource.cpp", + "LEGORacers/src/race/racer/racer.cpp", + "LEGORacers/src/race/racestate.cpp" + ], + "functions": [ + { + "name": "CheckpointGraph::Entry::Load", + "path": "LEGORacers/src/race/checkpointgraph.cpp", + "lines": [ + 42, + 81 + ] + }, + { + "name": "CheckpointGraph::AdvanceAlongGraph", + "path": "LEGORacers/src/race/checkpointgraphwalk.cpp", + "lines": [ + 6, + 45 + ] + }, + { + "name": "LapZoneResource::OnStartForRacer", + "path": "LEGORacers/src/race/events/lapzoneresource.cpp", + "lines": [ + 50, + 67 + ] + }, + { + "name": "Racer::CrossFinishLine", + "path": "LEGORacers/src/race/racer/racer.cpp", + "lines": [ + 2250, + 2283 + ] + }, + { + "name": "Racer::OnCheckpointCrossed", + "path": "LEGORacers/src/race/racer/racer.cpp", + "lines": [ + 2435, + 2483 + ] + }, + { + "name": "Racer::GetRaceProgress", + "path": "LEGORacers/src/race/racer/racer.cpp", + "lines": [ + 2484, + 2493 + ] + }, + { + "name": "RaceState::UpdateStandings", + "path": "LEGORacers/src/race/racestate.cpp", + "lines": [ + 637, + 801 + ] + } + ], + "dependsOn": [] + }, + { + "id": "hazards", + "name": "Track hazards", + "purpose": "Event-driven hazard objects: world-space trigger volumes carried by animated track pieces, with impact events fired at chosen animation frames", + "paths": [ + "LEGORacers/src/race/hazardmanager.cpp", + "LEGORacers/src/race/hazards/hazard.cpp", + "LEGORacers/src/race/hazards/movingobstaclehazard.cpp" + ], + "functions": [ + { + "name": "HazardManager::Update", + "path": "LEGORacers/src/race/hazardmanager.cpp", + "lines": [ + 379, + 388 + ] + }, + { + "name": "HazardManager::DispatchEventStart", + "path": "LEGORacers/src/race/hazardmanager.cpp", + "lines": [ + 305, + 325 + ] + }, + { + "name": "MovingObstacleHazard::OnActivate", + "path": "LEGORacers/src/race/hazards/movingobstaclehazard.cpp", + "lines": [ + 109, + 134 + ] + }, + { + "name": "MovingObstacleHazard::Update", + "path": "LEGORacers/src/race/hazards/movingobstaclehazard.cpp", + "lines": [ + 151, + 199 + ] + }, + { + "name": "MovingObstacleHazard::UpdatePerRacer", + "path": "LEGORacers/src/race/hazards/movingobstaclehazard.cpp", + "lines": [ + 200, + 242 + ] + } + ], + "dependsOn": [] + }, + { + "id": "race-loop", + "name": "Race session loop", + "purpose": "Frame pacing, the fixed simulation substep, the countdown that starts a race and the per-frame ordering of input, racer updates and camera updates", + "paths": [ + "LEGORacers/src/race/racesession.cpp", + "LEGORacers/src/race/racestate.cpp", + "LEGORacers/src/app/golapp.cpp", + "LEGORacers/src/app/win32golapp.cpp", + "LEGORacers/include/race/racesession.h", + "LEGORacers/include/app/win32golapp.h" + ], + "functions": [ + { + "name": "RaceSession::Update", + "path": "LEGORacers/src/race/racesession.cpp", + "lines": [ + 2032, + 2108 + ] + }, + { + "name": "RaceSession::Run", + "path": "LEGORacers/src/race/racesession.cpp", + "lines": [ + 381, + 484 + ] + }, + { + "name": "RaceSession::UpdateCountdownState", + "path": "LEGORacers/src/race/racesession.cpp", + "lines": [ + 1774, + 1846 + ] + }, + { + "name": "RaceState::UpdateRacers", + "path": "LEGORacers/src/race/racestate.cpp", + "lines": [ + 587, + 636 + ] + }, + { + "name": "GolApp::ResetRuntimeState", + "path": "LEGORacers/src/app/golapp.cpp", + "lines": [ + 9, + 32 + ] + }, + { + "name": "Win32GolApp::Tick", + "path": "LEGORacers/src/app/win32golapp.cpp", + "lines": [ + 430, + 465 + ] + } + ], + "dependsOn": [ + "car-body", + "drive-controller", + "player-input" + ] + } + ], + "cards": [ + { + "id": "throttle-thrust", + "node": "drive-controller", + "name": "Throttle, brake and turbo thrust", + "summary": "One thrust scalar per racer, doubled into a brake when the input fights the direction of travel, and replaced wholesale while a turbo runs", + "behavior": "At full forward input the controller hands the body a thrust of 54 units; any partial input scales that value linearly. When the requested direction opposes the current forward speed the same value is multiplied by 2 and treated as a brake, so lifting off and pulling back reverses more sharply than it accelerates. A scripted brake-to-stop path simply asks for twice full thrust in the opposite direction. Reverse while still pressing forward is halved to 0.5 so a stall does not fire the car backwards at full authority. A turbo ignores the throttle entirely and sets thrust to eight times full throttle, and while the car is scraping a wall the turbo is flagged as weakened and only half of that figure is applied. The value reaches the body through a setter that also recomputes the drag coefficient, so changing thrust immediately changes the terminal speed.", + "constants": [ + { + "name": "fullThrottleThrust", + "value": 54, + "unit": "thrust force units per update (fixed 40 ms simulation step)", + "source": "LEGORacers/src/race/racer/drivecontroller.cpp:19" + }, + { + "name": "brakeThrustScale", + "value": 2, + "unit": "multiplier applied to thrust while braking", + "source": "LEGORacers/src/race/racer/drivecontroller.cpp:22" + }, + { + "name": "turboThrustScale", + "value": 8, + "unit": "multiplier over full throttle", + "source": "LEGORacers/src/race/racer/drivecontroller.cpp:61" + }, + { + "name": "weakenedTurboThrustScale", + "value": 0.5, + "unit": "multiplier over turbo thrust while touching a wall", + "source": "LEGORacers/src/race/racer/drivecontroller.cpp:353" + }, + { + "name": "reverseWhileThrottlingScale", + "value": 0.5, + "unit": "multiplier on the drive signal", + "source": "LEGORacers/src/race/playercontrols.cpp:18" + } + ], + "sources": [ + { + "path": "LEGORacers/src/race/racer/drivecontroller.cpp", + "line": 318 + }, + { + "path": "LEGORacers/src/race/racer/drivecontroller.cpp", + "line": 341 + }, + { + "path": "LEGORacers/src/race/racer/drivecontroller.cpp", + "line": 175 + }, + { + "path": "LEGORacers/src/race/racer/racercarbody.cpp", + "line": 2308 + } + ], + "capabilities": [ + "analogue throttle", + "braking", + "turbo thrust override" + ], + "tags": [ + "movement", + "throttle", + "brake", + "boost" + ] + }, + { + "id": "steering-radius", + "node": "drive-controller", + "name": "Steering as a requested turn radius", + "summary": "Stick position maps into curvature between a tight and a wide radius, softened at very low speed and clipped by the grip the tyres actually have", + "behavior": "Steering never sets a yaw rate directly. The shaped input is multiplied by the handling scale and then interpolated between an inverse radius of 0.025 and one of 0.00025, so a full deflection asks for the tightest arc the model allows, about 40 units, while small deflections ask for something close to a straight line. A radius larger than 4096 is treated as straight. Below a speed of 0.04 the requested radius is multiplied by 0.2 so a parked car cannot pivot on the spot. The physics layer clamps the request to the same 40 to 4096 band and the drive controller then compares the request against the minimum radius the current surface grip can support: if the request is tighter it is relaxed toward that limit and the wheels are put into a steering state whose gain rises as the requested radius tightens, using a slip ratio of 0.85 and a maximum facing offset of 0.7071 radians. When the tyres cannot support the turn at all the steering state is dropped instead. Handling from the car build enters as 0.003 per stat point plus a 0.7 base, so a strong handling stat tightens every requested radius.", + "constants": [ + { + "name": "tightestInverseRadius", + "value": 0.025, + "unit": "inverse world units (one over 40 units)", + "source": "LEGORacers/src/race/racer/drivecontroller.cpp:13" + }, + { + "name": "widestInverseRadius", + "value": 0.00025, + "unit": "inverse world units (one over 4096 units)", + "source": "LEGORacers/src/race/racer/drivecontroller.cpp:16" + }, + { + "name": "straightRadiusCutoff", + "value": 4096, + "unit": "world units of turn radius", + "source": "LEGORacers/src/race/racer/drivecontroller.cpp:10" + }, + { + "name": "lowSpeedSteerThreshold", + "value": 0.04, + "unit": "speed units per update", + "source": "LEGORacers/src/race/racer/drivecontroller.cpp:25" + }, + { + "name": "lowSpeedSteerScale", + "value": 0.2, + "unit": "multiplier on the requested radius", + "source": "LEGORacers/src/race/racer/drivecontroller.cpp:28" + }, + { + "name": "minimumTurnRadius", + "value": 40, + "unit": "world units of turn radius", + "source": "LEGORacers/src/race/racer/racercarbody.cpp:43" + }, + { + "name": "straightSteerSlipRatio", + "value": 0.85, + "unit": "fraction of velocity lost along the slip direction", + "source": "LEGORacers/src/race/racer/drivecontroller.cpp:299" + }, + { + "name": "straightSteerMaxFacingOffset", + "value": 0.70709997, + "unit": "radians", + "source": "LEGORacers/src/race/racer/drivecontroller.cpp:299" + }, + { + "name": "handlingStatScale", + "value": 0.003, + "unit": "handling multiplier per stat point", + "source": "LEGORacers/src/race/racer/racerphysics.cpp:160" + } + ], + "sources": [ + { + "path": "LEGORacers/src/race/racer/drivecontroller.cpp", + "line": 220 + }, + { + "path": "LEGORacers/src/race/racer/drivecontroller.cpp", + "line": 240 + }, + { + "path": "LEGORacers/src/race/racer/racercarbody.cpp", + "line": 859 + }, + { + "path": "LEGORacers/src/race/racer/racercarbody.cpp", + "line": 1505 + }, + { + "path": "LEGORacers/src/race/racer/racerphysics.cpp", + "line": 1397 + } + ], + "capabilities": [ + "curvature-based steering", + "grip-limited turning", + "speed-dependent authority" + ], + "tags": [ + "movement", + "steering", + "handling", + "feel" + ] + }, + { + "id": "stat-scaling", + "node": "racer-physics", + "name": "Car build stat scaling", + "summary": "Three stat values scale handling, acceleration and top speed, and the boosted ceiling is a separate figure entirely", + "behavior": "Every chassis carries handling, acceleration and top-speed stats that default to 33. Handling becomes a multiplier of 0.003 per point plus 0.7, so the default sits just under 0.8 and the scale runs from 0.7 at zero to 0.85 at fifty. Acceleration and top speed share one shape: one minus a hundredth of the distance below 50, so the default keeps 0.983 of the base figure and only a stat of 50 leaves it untouched. Setting the top-speed stat also clears any running boost, which is what makes a garage change take effect immediately rather than after the current boost expires. Boost is not just extra force: entering one raises the speed ceiling from 120 to 176 and multiplies the recorded lateral grip by 1.5, so the car turns as well as it accelerates, and leaving the boost restores both figures. The vertical acceleration the whole model leans on is 39 units of acceleration, which sets how support, friction and drag forces read against body mass.", + "constants": [ + { + "name": "handlingStatScale", + "value": 0.003, + "unit": "handling multiplier per stat point", + "source": "LEGORacers/src/race/racer/racerphysics.cpp:160" + }, + { + "name": "handlingStatOffset", + "value": 0.7, + "unit": "handling multiplier base", + "source": "LEGORacers/src/race/racer/racerphysics.cpp:1339" + }, + { + "name": "statPenaltyScale", + "value": 0.001, + "unit": "multiplier per stat point below the reference", + "source": "LEGORacers/src/race/racer/racerphysics.cpp:1346" + }, + { + "name": "statReferenceValue", + "value": 50, + "unit": "stat points", + "source": "LEGORacers/src/race/racer/racerphysics.cpp:1346" + }, + { + "name": "defaultStatValue", + "value": "0x21", + "unit": "stat points (33)", + "source": "LEGORacers/src/race/racer/racerphysics.cpp:184" + }, + { + "name": "baseSpeedCeiling", + "value": 120, + "unit": "speed units per update", + "source": "LEGORacers/src/race/racer/racerphysics.cpp:46" + }, + { + "name": "boostedSpeedCeiling", + "value": 176, + "unit": "speed units per update", + "source": "LEGORacers/src/race/racer/racerphysics.cpp:49" + }, + { + "name": "boostGripScale", + "value": 1.5, + "unit": "multiplier on recorded lateral grip", + "source": "LEGORacers/src/race/racer/racerphysics.cpp:277" + }, + { + "name": "bodyGravity", + "value": 39, + "unit": "world units per millisecond squared of acceleration", + "source": "LEGORacers/src/race/racer/racercarbody.cpp:55" + } + ], + "sources": [ + { + "path": "LEGORacers/src/race/racer/racerphysics.cpp", + "line": 1336 + }, + { + "path": "LEGORacers/src/race/racer/racerphysics.cpp", + "line": 1350 + }, + { + "path": "LEGORacers/src/race/racer/racerphysics.cpp", + "line": 265 + }, + { + "path": "LEGORacers/src/race/racer/racerphysics.cpp", + "line": 283 + } + ], + "capabilities": [ + "stat-driven tuning", + "boost ceilings", + "grip scaling" + ], + "tags": [ + "stats", + "tuning", + "boost", + "physics" + ] + }, + { + "id": "powerslide", + "node": "car-body", + "name": "Powerslide and release boost", + "summary": "A drift button turns four wheel probes into two axle probes, lifts the body, banks it and multiplies thrust on release", + "behavior": "A powerslide only opens when the car is not touching a wall, at least three wheels are grounded or it is already sliding, the travel direction still points within a cosine of 0.85 of the facing axis, and forward speed clears 0.05. Holding the drift button while turning enters the slide state, which switches ground detection from four wheel rays to two rays cast at the mid points of the front and rear axles, with a differently scaled sweep and a lift term added to the ray length. The body's visual bank angle is driven toward a target proportional to the inverse of the requested radius, clamped to 0.785 radians, moving at 0.005 radians per millisecond of simulation time. Steering while sliding uses the minimum radius the surface can support: the assist factor is one minus the ratio of requested to achievable radius, floored at 0.05, and the resulting gain and slip differ for a left and right handed slide. Releasing the drift clears the slide and the powerslide flags, and a separate release-boost flag, when it is set, multiplies thrust by 8 while it survives and expires after 300 ms, so a clean slide pays out as a short strong push rather than a permanent speed increase. At this commit the decompilation tests and clears that flag in the controller but does not visibly raise it, so the trigger itself is not yet matched to source.", + "constants": [ + { + "name": "powerslideMinimumSpeed", + "value": 0.050000001, + "unit": "speed units per update", + "source": "LEGORacers/src/race/racer/racerphysics.cpp:70" + }, + { + "name": "powerslideAlignmentMinimum", + "value": 0.85000002, + "unit": "cosine between travel and facing", + "source": "LEGORacers/src/race/racer/racerphysics.cpp:163" + }, + { + "name": "slideLiftTarget", + "value": 6, + "unit": "world units of ray extension", + "source": "LEGORacers/src/race/racer/racercarbody.cpp:91" + }, + { + "name": "slideLiftRate", + "value": 3, + "unit": "lift units per millisecond of simulation step", + "source": "LEGORacers/src/race/racer/racercarbody.cpp:94" + }, + { + "name": "slideSweepScale", + "value": 0.01, + "unit": "sweep units per millisecond of simulation step", + "source": "LEGORacers/src/race/racer/racercarbody.cpp:76" + }, + { + "name": "slideBankMaximum", + "value": 0.78539819, + "unit": "radians of body roll", + "source": "LEGORacers/src/race/racer/racercarbody.cpp:112" + }, + { + "name": "slideBankRate", + "value": 0.005, + "unit": "radians per millisecond of simulation step", + "source": "LEGORacers/src/race/racer/racercarbody.cpp:97" + }, + { + "name": "slideSteerAssistMinimum", + "value": 0.050000001, + "unit": "fraction of available turn", + "source": "LEGORacers/src/race/racer/drivecontroller.cpp:37" + }, + { + "name": "slideSteerSlipRatio", + "value": 0.85, + "unit": "fraction of velocity lost along the slip direction", + "source": "LEGORacers/src/race/racer/drivecontroller.cpp:282" + }, + { + "name": "slideReleaseThrustScale", + "value": 8, + "unit": "multiplier over current throttle thrust", + "source": "LEGORacers/src/race/racer/drivecontroller.cpp:347" + }, + { + "name": "slideReleaseWindow", + "value": 300, + "unit": "milliseconds", + "source": "LEGORacers/src/race/racer/drivecontroller.cpp:157" + } + ], + "sources": [ + { + "path": "LEGORacers/src/race/racer/drivecontroller.cpp", + "line": 372 + }, + { + "path": "LEGORacers/src/race/racer/drivecontroller.cpp", + "line": 392 + }, + { + "path": "LEGORacers/src/race/racer/racercarbody.cpp", + "line": 1192 + }, + { + "path": "LEGORacers/src/race/racer/racercarbody.cpp", + "line": 2419 + }, + { + "path": "LEGORacers/src/race/racer/racerphysics.cpp", + "line": 1364 + } + ], + "capabilities": [ + "two-axle ground probing", + "body lift and bank", + "release boost" + ], + "tags": [ + "drift", + "handling", + "boost", + "feel" + ] + }, + { + "id": "drag-speed-cap", + "node": "car-body", + "name": "Quadratic drag sets the top speed", + "summary": "The speed ceiling is enforced by a drag coefficient derived from mass, thrust and the ceiling itself, not by clamping velocity", + "behavior": "Whenever thrust or the speed ceiling is set, the body recomputes a drag coefficient as mass times thrust divided by the ceiling squared, and the drag force applied each step is that coefficient times speed, so the drag grows with the square of velocity and the equilibrium speed lands on the requested ceiling by construction. That is why the car accelerates hard from rest and then tapers: it is approaching a force balance, not a clamp. Rolling resistance from the surfaces under the wheels is summed over all four probes, scaled by a quarter and added to the same coefficient. Tyre scrub is applied separately: with steering or spinning active, a force of minus gravity times mass scale times surface friction times slip ratio acts along the velocity direction, and the remaining contact scale becomes one minus the slip ratio. Two more terms shape the low speed band, one proportional to lateral velocity and one that bleeds forward velocity when no throttle is held, both scaled by body mass. A resting body is finally snapped to exactly zero velocity once the integrated force is zero and the speed has fallen under a vanishing threshold.", + "constants": [ + { + "name": "defaultSpeedCeiling", + "value": 110, + "unit": "speed units per update", + "source": "LEGORacers/src/race/racer/racercarbody.cpp:64" + }, + { + "name": "lateralScrubScale", + "value": -0.0099999998, + "unit": "mass-scaled force per speed unit per update", + "source": "LEGORacers/src/race/racer/racercarbody.cpp:671" + }, + { + "name": "coastBleedScale", + "value": -0.001, + "unit": "mass-scaled force per speed unit per update", + "source": "LEGORacers/src/race/racer/racercarbody.cpp:701" + }, + { + "name": "rollingResistanceAverage", + "value": 0.25, + "unit": "fraction of the summed wheel resistance", + "source": "LEGORacers/src/race/racer/racercarbody.cpp:797" + }, + { + "name": "surfaceForceShare", + "value": 0.25, + "unit": "fraction of the summed per-wheel surface force", + "source": "LEGORacers/src/race/racer/racercarbody.cpp:781" + }, + { + "name": "boxInertiaFactor", + "value": 0.083333336, + "unit": "inertia coefficient (one twelfth of mass times the summed squared extents)", + "source": "LEGORacers/src/race/racer/racerrigidbody.cpp:21" + }, + { + "name": "halfStepWeight", + "value": 0.5, + "unit": "fraction of the acceleration term added to the position step", + "source": "LEGORacers/src/race/racer/racerrigidbody.cpp:273" + } + ], + "sources": [ + { + "path": "LEGORacers/src/race/racer/racercarbody.cpp", + "line": 2322 + }, + { + "path": "LEGORacers/src/race/racer/racercarbody.cpp", + "line": 562 + }, + { + "path": "LEGORacers/src/race/racer/racerrigidbody.cpp", + "line": 256 + }, + { + "path": "LEGORacers/src/race/racer/racercarbody.cpp", + "line": 2601 + } + ], + "capabilities": [ + "terminal speed from drag", + "rolling resistance", + "tyre scrub" + ], + "tags": [ + "physics", + "speed", + "drag", + "tuning" + ] + }, + { + "id": "wheel-grounding", + "node": "car-body", + "name": "Wheel rays and ground snapping", + "summary": "Four downward rays pick contact points, the deepest one snaps the body to the surface and the tilt of the contact patch becomes the support normal", + "behavior": "Each update casts one ray per wheel from just above the wheel position downward. The ray length is the elapsed milliseconds scaled by 0.04 while the car was already grounded, and zero on the first step after leaving the ground, so a car that lands probes at its own height instead of shooting rays through the floor. The best hit minus a padding of 0.4 defines the contact distance; every wheel whose hit is within that distance counts as touching and contributes its triangle normal to a summed support normal. With three or more wheels down the support normal is replaced by the body's own up axis and all four wheels are marked as contacting, which is what keeps the car stable on curved geometry. The body is then translated so the selected wheel ends up one ride height of 0.2 above its own contact point, and the wheel positions are recomputed. With no hit at all the car keeps falling and loses 0.008 of vertical velocity per step. A landing after more than 400 ms airborne is tested against the support normal: a downward impact beyond 0.05 rebounds, scaled by 1.15 with four wheels down and 1.3 otherwise, and cancels the contacts so the car bounces instead of sticking. Support force is gravity times mass scale projected along the normal, and each contact carries a share of one over the contact count plus 8.", + "constants": [ + { + "name": "wheelContactPadding", + "value": 0.4, + "unit": "world units", + "source": "LEGORacers/src/race/racer/racercarbody.cpp:67" + }, + { + "name": "rideHeight", + "value": 0.2, + "unit": "world units above the contact point", + "source": "LEGORacers/src/race/racer/racercarbody.cpp:70" + }, + { + "name": "wheelSweepScale", + "value": 0.04, + "unit": "sweep units per millisecond of simulation step", + "source": "LEGORacers/src/race/racer/racercarbody.cpp:73" + }, + { + "name": "airborneDropStep", + "value": 0.008, + "unit": "world units per update of vertical velocity", + "source": "LEGORacers/src/race/racer/racercarbody.cpp:79" + }, + { + "name": "landingAirborneWindow", + "value": 400, + "unit": "milliseconds of airtime before a bounce is possible", + "source": "LEGORacers/src/race/racer/racercarbody.cpp:1113" + }, + { + "name": "landingBounceThreshold", + "value": 0.05, + "unit": "world units per update of impact speed", + "source": "LEGORacers/src/race/racer/racercarbody.cpp:100" + }, + { + "name": "landingBounceScale", + "value": 1.3, + "unit": "multiplier on the reflected impact speed", + "source": "LEGORacers/src/race/racer/racercarbody.cpp:103" + }, + { + "name": "landingBounceScaleFourWheel", + "value": 1.15, + "unit": "multiplier on the reflected impact speed with four wheels down", + "source": "LEGORacers/src/race/racer/racercarbody.cpp:106" + }, + { + "name": "contactForceDivisorOffset", + "value": 8, + "unit": "added to the contact count before dividing gravity", + "source": "LEGORacers/src/race/racer/racercarbody.cpp:1142" + } + ], + "sources": [ + { + "path": "LEGORacers/src/race/racer/racercarbody.cpp", + "line": 903 + }, + { + "path": "LEGORacers/src/race/racer/racercarbody.cpp", + "line": 2339 + }, + { + "path": "LEGORacers/src/race/racer/racercarbody.cpp", + "line": 2796 + }, + { + "path": "LEGORacers/src/race/racer/racercarbody.cpp", + "line": 1036 + } + ], + "capabilities": [ + "wheel raycasting", + "ground snapping", + "landing bounce" + ], + "tags": [ + "physics", + "grounding", + "jump", + "feel" + ] + }, + { + "id": "surface-grip", + "node": "racer-physics", + "name": "Per-surface grip, friction and support", + "summary": "Every surface triangle can override lateral grip, friction and the support threshold, and the wheels average them into the force model", + "behavior": "Track geometry carries a surface description per material, and each wheel hit copies four values out of it: a support threshold, a friction value, a lateral grip value and a rolling resistance, together with an optional constant force and a wheel particle. Missing values fall back to 3.0 for lateral grip, 0.25 for friction and 0.5 for the support threshold, so a plain surface still drives normally. Support force is only applied when its magnitude exceeds gravity times mass scale times the average threshold, which is how a loose surface makes the car sink or skate. Friction and lateral grip are averaged over the four probes and used in the scrub term and in the minimum-radius calculation, where lateral grip times gravity times mass scale is the centripetal budget and the radius is speed squared times mass over that budget. Entering and leaving a surface also fires events, and the paint on the surface changes the looping rolling sound, so a surface swap is audible before it is visible. Wheel particles only spawn above a forward speed of 0.008 and never while sliding, spinning or on a computer-driven car.", + "constants": [ + { + "name": "defaultLateralGrip", + "value": 3, + "unit": "grip units used in the centripetal budget", + "source": "LEGORacers/src/race/racer/racerphysics.cpp:55" + }, + { + "name": "defaultSupportThreshold", + "value": 0.5, + "unit": "fraction of gravity required before support is applied", + "source": "LEGORacers/src/race/racer/racerphysics.cpp:61" + }, + { + "name": "defaultFriction", + "value": 0.25, + "unit": "friction coefficient per wheel", + "source": "LEGORacers/src/race/racer/racerphysics.cpp:64" + }, + { + "name": "wheelParticleMinimumSpeed", + "value": 0.008, + "unit": "speed units per update", + "source": "LEGORacers/src/race/racer/racerphysics.cpp:67" + }, + { + "name": "wheelAverageDivisor", + "value": 0.25, + "unit": "fraction applied to the summed wheel values", + "source": "LEGORacers/src/race/racer/racercarbody.cpp:2597" + }, + { + "name": "boostGripScale", + "value": 1.5, + "unit": "multiplier on recorded lateral grip", + "source": "LEGORacers/src/race/racer/racerphysics.cpp:277" + }, + { + "name": "fallbackTurnRadius", + "value": 100, + "unit": "world units of turn radius when airborne", + "source": "LEGORacers/src/race/racer/racercarbody.cpp:133" + } + ], + "sources": [ + { + "path": "LEGORacers/src/race/racer/racerphysics.cpp", + "line": 1118 + }, + { + "path": "LEGORacers/src/race/racer/racerphysics.cpp", + "line": 1221 + }, + { + "path": "LEGORacers/src/race/racer/racercarbody.cpp", + "line": 859 + }, + { + "path": "LEGORacers/src/race/racer/racercarbody.cpp", + "line": 2614 + }, + { + "path": "LEGORacers/src/race/surfacetable.cpp", + "line": 21 + } + ], + "capabilities": [ + "material-driven grip", + "support threshold", + "surface events" + ], + "tags": [ + "physics", + "grip", + "surface", + "off-road" + ] + }, + { + "id": "wall-collision", + "node": "car-body", + "name": "Wall hits and body points", + "summary": "Body corner segments are swept against track geometry every step, and a hit rewinds the step, reflects velocity and kicks the nose off the wall", + "behavior": "Four body points are projected into world space and each is swept from its previous position to its current one against the track collider. The deepest hit wins, and the step is rewound to just before the impact: the remaining time is the elapsed milliseconds times the reported hit fraction minus 5, and anything under 5 ms is treated as fully consumed. The response removes the component of velocity going into the wall, then damps it twice more, taking 0.3 along the wall's horizontal normal and 0.15 of the vertical part, and adds a small push of 0.004 along the normal so the car separates. Vertical velocity is capped at 0.3 so a wall hit can never launch the car upward. If the wall normal opposes the body's right axis the nose is yawed away with an impulse of up to 0.004 plus a sign term for 200 ms, and any steering state is dropped on contact. The whole loop repeats after a rewind so a car pinned between two surfaces still resolves the rest of its step.", + "constants": [ + { + "name": "wallHorizontalDamping", + "value": 0.30000001, + "unit": "fraction of the into-wall speed removed", + "source": "LEGORacers/src/race/racer/racercarbody.cpp:82" + }, + { + "name": "wallVerticalDamping", + "value": 0.15000001, + "unit": "fraction of the into-wall vertical speed removed", + "source": "LEGORacers/src/race/racer/racercarbody.cpp:85" + }, + { + "name": "wallPushoutScale", + "value": 0.0040000002, + "unit": "velocity units along the wall normal per update", + "source": "LEGORacers/src/race/racer/racercarbody.cpp:88" + }, + { + "name": "wallMaximumRiseSpeed", + "value": 0.30000001, + "unit": "world units per update of upward velocity", + "source": "LEGORacers/src/race/racer/racercarbody.cpp:130" + }, + { + "name": "wallMinStepRemainder", + "value": 5, + "unit": "milliseconds", + "source": "LEGORacers/src/race/racer/racercarbody.cpp:2019" + }, + { + "name": "wallNoseKickDuration", + "value": 200, + "unit": "milliseconds", + "source": "LEGORacers/src/race/racer/racercarbody.cpp:2099" + } + ], + "sources": [ + { + "path": "LEGORacers/src/race/racer/racercarbody.cpp", + "line": 1884 + }, + { + "path": "LEGORacers/src/race/racer/racercarbody.cpp", + "line": 2030 + }, + { + "path": "LEGORacers/src/race/racer/racercarbody.cpp", + "line": 2901 + }, + { + "path": "LEGORacers/src/race/racer/racerphysics.cpp", + "line": 1081 + } + ], + "capabilities": [ + "swept body collision", + "impact rewind", + "wall nose kick" + ], + "tags": [ + "collision", + "walls", + "physics", + "feel" + ] + }, + { + "id": "upright-clamp", + "node": "car-body", + "name": "Upright clamp and visual bank", + "summary": "A pitch limit keeps the chassis from ever tipping past about forty five degrees, while a separate visual bank leans the body in slides", + "behavior": "After every integration step the body checks the forward axis: if its up component has fallen below 0.7071, meaning the chassis is pitched past roughly 45 degrees, the forward axis is rebuilt on a horizontal axis with the up component forced to the cosine of 0.7854 radians and the body is re-based on it. That clamps nose-dives and wheelies without touching velocity, so a hard landing or an explosion can rotate the car visibly but never leave it inverted or stuck on its roof. Separately the render-side bank target is chosen by state: a slide uses the negative of the sliding bank angle, a powerslide uses the inverse of the requested radius scaled by the tightest radius, by the slide factor and by the ratio of current speed to the speed ceiling, clamped to 0.785 radians, and any other state returns to level. The clamped target is then approached with a rate limited smoothing, so the lean reads as weight transfer rather than a snap.", + "constants": [ + { + "name": "maximumTiltAngle", + "value": 0.78539819, + "unit": "radians", + "source": "LEGORacers/src/race/racer/racercarbody.cpp:58" + }, + { + "name": "minimumUprightCosine", + "value": 0.70710677, + "unit": "cosine of the forward axis above the horizontal plane", + "source": "LEGORacers/src/race/racer/racercarbody.cpp:61" + } + ], + "sources": [ + { + "path": "LEGORacers/src/race/racer/racercarbody.cpp", + "line": 878 + }, + { + "path": "LEGORacers/src/race/racer/racercarbody.cpp", + "line": 482 + }, + { + "path": "LEGORacers/src/race/racer/racercarbody.cpp", + "line": 2519 + } + ], + "capabilities": [ + "pitch clamping", + "self righting", + "visual lean" + ], + "tags": [ + "physics", + "stability", + "feel", + "visuals" + ] + }, + { + "id": "spin-out", + "node": "car-body", + "name": "Spin reactions and spin-out", + "summary": "Hits either spin the chassis for a counted number of turns or lock it into a timed, unsteerable spin-out", + "behavior": "A spin is expressed in turns and a rate, and is converted into a yaw impulse duration: the full turn time is one revolution divided by the rate and the impulse lasts that period times the turn count, so a two turn spin lasts twice as long. While spinning the steering state is cleared, the accumulated angular momentum along the body up axis is cancelled first so the spin starts from a known state, and the facing used for thrust is no longer produced from a requested turn radius, so nothing fights the rotation. A shield contact spins at 0.0089999996 radians per millisecond, which turns one revolution in roughly 700 ms, so a two turn hit holds the car for about 1.4 seconds; hazards spin at 0.01 and powerup hits at 0.007, so the same one or two turn reaction reads differently depending on what caused it. A spin-out is a separate, harsher state: it lasts 600 ms, makes the ground probe treat every step as a fresh landing, skips the per-wheel surface forces and blocks ordinary steering, and on the trajectory-driven path it also gives the car an upward jump of 50 units and drops its playback speed to 0.1, with a gravity of -100 pulling it back down and clearing the state at ground level. Ordinary driving can never spin the chassis; only hits, hazards and shield contacts can.", + "constants": [ + { + "name": "spinOutDuration", + "value": 600, + "unit": "milliseconds", + "source": "LEGORacers/src/race/racer/racercarbody.cpp:2507" + }, + { + "name": "oneTurnSpinRate", + "value": 0.0089999996, + "unit": "radians per millisecond", + "source": "LEGORacers/src/race/racer/racer.cpp:1580" + }, + { + "name": "twoTurnSpinCount", + "value": 2, + "unit": "turns", + "source": "LEGORacers/src/race/racer/racer.cpp:1583" + }, + { + "name": "routeSpinOutJumpVelocity", + "value": 50, + "unit": "world units per second of vertical velocity", + "source": "LEGORacers/src/race/racer/racerphysics.cpp:145" + }, + { + "name": "routeSpinOutPlaybackSpeed", + "value": 0.1, + "unit": "playback speed fraction of the normal route rate", + "source": "LEGORacers/src/race/racer/racerphysics.cpp:91" + }, + { + "name": "routeJumpGravity", + "value": -100, + "unit": "world units per second squared", + "source": "LEGORacers/src/race/racer/racerphysics.cpp:148" + }, + { + "name": "hazardSpinRate", + "value": 0.01, + "unit": "radians per millisecond", + "source": "LEGORacers/src/race/hazards/movingobstaclehazard.cpp:256" + }, + { + "name": "projectileSpinRate", + "value": 0.007, + "unit": "radians per millisecond", + "source": "LEGORacers/src/race/powerups/homingmissileaction.cpp:483" + } + ], + "sources": [ + { + "path": "LEGORacers/src/race/racer/racercarbody.cpp", + "line": 2227 + }, + { + "path": "LEGORacers/src/race/racer/racercarbody.cpp", + "line": 2505 + }, + { + "path": "LEGORacers/src/race/racer/racerphysics.cpp", + "line": 680 + }, + { + "path": "LEGORacers/src/race/racer/racer.cpp", + "line": 1578 + } + ], + "capabilities": [ + "multi turn spin", + "timed spin-out", + "untouchable recovery window" + ], + "tags": [ + "combat", + "hit reaction", + "recovery", + "feel" + ] + }, + { + "id": "car-contact-impulse", + "node": "racer-contact", + "name": "Car to car collision resolution", + "summary": "Racers exchange an impulse computed from relative velocity, contact offset and inverse inertia, then apply shield rules", + "behavior": "When two chassis touch, neither ghosted nor already spun out, the contacting car is pushed out of the other along the contact normal by the penetration depth. The impulse magnitude is the closing speed along the normal scaled by 1.75, which is a restitution of 0.75, divided by the sum of inverse masses plus the angular contribution at the contact point; each car then receives that impulse along the normal with opposite signs and light force feedback fires on both. The angular terms use each body's world inverse inertia at its centre of mass, so a hit on a corner rotates the car much more than a hit on the nose. Shield rules run after the impulse: a shielded car that is struck spins or shoves an unshielded opponent depending on its level, and a level one shield shoves only when the contact lies off the opponent's long axis, with the cosine of that axis under 0.7, using a 200 unit impulse that then acts as an external force for 750 ms. A shielded car also hands a curse over to an unshielded one instead of keeping it, and touching a cursed car transfers the curse to the other car. Computer-driven cars additionally receive a 750 ms speed ramp flag, and both cars take a 250 ms scrape-sound cooldown so a long contact does not machine gun the audio.", + "constants": [ + { + "name": "collisionRestitution", + "value": 0.75, + "unit": "bounce coefficient", + "source": "LEGORacers/src/race/racer/racer.cpp:105" + }, + { + "name": "shieldShoveConeCosine", + "value": 0.69999999, + "unit": "cosine of the shove cone half angle", + "source": "LEGORacers/src/race/racer/racer.cpp:144" + }, + { + "name": "shieldShoveStrength", + "value": 200, + "unit": "impulse units", + "source": "LEGORacers/src/race/racer/racer.cpp:171" + }, + { + "name": "shoveDuration", + "value": 750, + "unit": "milliseconds", + "source": "LEGORacers/src/race/racer/racer.cpp:1638" + }, + { + "name": "shoveSteeringGain", + "value": 2.5, + "unit": "steering gain while shoved", + "source": "LEGORacers/src/race/racer/racer.cpp:1643" + }, + { + "name": "shoveSlipRatio", + "value": 0.89999998, + "unit": "fraction of velocity lost along the slip direction", + "source": "LEGORacers/src/race/racer/racer.cpp:1643" + }, + { + "name": "speedRampDuration", + "value": 750, + "unit": "milliseconds", + "source": "LEGORacers/src/race/racer/racer.cpp:1519" + }, + { + "name": "scrapeSoundCooldown", + "value": 250, + "unit": "milliseconds", + "source": "LEGORacers/src/race/racer/racer.cpp:1548" + } + ], + "sources": [ + { + "path": "LEGORacers/src/race/racer/racer.cpp", + "line": 1365 + }, + { + "path": "LEGORacers/src/race/racer/racer.cpp", + "line": 1427 + }, + { + "path": "LEGORacers/src/race/racer/racer.cpp", + "line": 1566 + }, + { + "path": "LEGORacers/src/race/racer/racer.cpp", + "line": 1627 + } + ], + "capabilities": [ + "impulse collision", + "restitution", + "shield shove" + ], + "tags": [ + "combat", + "collision", + "physics", + "feel" + ] + }, + { + "id": "chase-camera", + "node": "race-camera", + "name": "Chase camera follow", + "summary": "Camera sits on a pitch and height offset behind the car, leads into turns and lags behind both position and rotation", + "behavior": "The chase camera builds a flat view direction from the car's facing, then adds a look-ahead turn amount equal to forward speed times 80 divided by the requested turn radius, clamped to plus or minus 0.3 radians, smoothed at 0.00444 per millisecond so the lead eases in rather than snapping. The resulting angle is compared against the current view angle and only corrected once the error escapes a 0.014 radian dead zone, using an exponential amount of one over the view rate times elapsed plus one with the rate at 0.0167 per millisecond. The camera then sits at the follow distance of the active view profile, offset by that profile's pitch and height sines, and the whole transform is smoothed with the same exponential form for position and for quaternion rotation, which is what makes the camera glide instead of sticking to the car. Turbo changes the distance briefly: levels zero, one and two pull the camera 0.2, 0.4 and 0.6 of the follow distance closer, while the top level just keeps the standard distance, and the scale eases back at a brick-settling rate. A cockpit mode attaches the camera directly to the car axes and a look-back mode mirrors it about the car.", + "constants": [ + { + "name": "defaultPositionFollowRate", + "value": 0.09, + "unit": "lag coefficient per millisecond", + "source": "LEGORacers/src/race/racecameracontroller.cpp:329" + }, + { + "name": "defaultTurnLeadRate", + "value": 0.0044444446, + "unit": "lag coefficient per millisecond", + "source": "LEGORacers/src/race/racecameracontroller.cpp:330" + }, + { + "name": "defaultViewAngleRate", + "value": 0.016666668, + "unit": "lag coefficient per millisecond", + "source": "LEGORacers/src/race/racecameracontroller.cpp:331" + }, + { + "name": "turnLookAheadScale", + "value": 80, + "unit": "lead angle per unit of speed over radius", + "source": "LEGORacers/src/race/racecameracontroller.cpp:63" + }, + { + "name": "turnLeadClamp", + "value": -0.30000001, + "unit": "radians", + "source": "LEGORacers/src/race/racecameracontroller.cpp:60" + }, + { + "name": "viewAngleDeadband", + "value": 0.014, + "unit": "radians", + "source": "LEGORacers/src/race/racecameracontroller.cpp:54" + }, + { + "name": "minimumCameraDistance", + "value": 12, + "unit": "world units from the car", + "source": "LEGORacers/src/race/racecameracontroller.cpp:48" + }, + { + "name": "lowestFollowDistance", + "value": 5, + "unit": "world units from the car", + "source": "LEGORacers/src/race/racecameracontroller.cpp:28" + }, + { + "name": "widestFollowDistance", + "value": 45, + "unit": "world units from the car", + "source": "LEGORacers/src/race/racecameracontroller.cpp:34" + }, + { + "name": "turboPullInLevel0", + "value": -0.2, + "unit": "fraction of follow distance", + "source": "LEGORacers/src/race/racecameracontroller.cpp:658" + }, + { + "name": "turboPullInLevel2", + "value": -0.6, + "unit": "fraction of follow distance", + "source": "LEGORacers/src/race/racecameracontroller.cpp:664" + } + ], + "sources": [ + { + "path": "LEGORacers/src/race/racecameracontroller.cpp", + "line": 487 + }, + { + "path": "LEGORacers/src/race/racecameracontroller.cpp", + "line": 291 + }, + { + "path": "LEGORacers/src/race/racecameracontroller.cpp", + "line": 440 + }, + { + "path": "LEGORacers/src/race/racecameracontroller.cpp", + "line": 154 + } + ], + "capabilities": [ + "chase camera", + "turn lead", + "exponential lag", + "turbo distance" + ], + "tags": [ + "camera", + "feel", + "boost", + "smoothing" + ] + }, + { + "id": "camera-shake", + "node": "race-camera", + "name": "Speed shake and view transitions", + "summary": "Field of view is shaken on a cosine ramp for half a second, and profile changes are cross faded instead of cut", + "behavior": "A short-lived field-of-view wobble provides speed feedback. When the shake window has expired the camera re-arms it for 500 ms and records the base field of view; on a car flagged as boosting it then picks a random offset that keeps the field of view inside a band from 40 to 80 degrees, either pulling away from the ceiling or pushing off the floor, and otherwise it uses half the difference between the base and the target field of view. The offset is read out as a cosine of the remaining time scaled by 0.02, by 0.1 and by pi, so the wobble is a smooth lobe rather than jitter, and the projection is marked dirty on every frame it runs. The finish camera blends over a 2000 ms window, mixing follow distance, pitch and height sine from the previous profile into the new one and sweeping a side offset round at 0.00157 radians per millisecond, so a switch to the victory angle orbits instead of popping. Audio listener position, velocity and basis are refreshed from the same camera object each frame, so the mix follows the view.", + "constants": [ + { + "name": "shakeFovCeiling", + "value": 80, + "unit": "degrees of field of view", + "source": "LEGORacers/src/race/racecameracontroller.cpp:15" + }, + { + "name": "shakeFovFloor", + "value": 40, + "unit": "degrees of field of view", + "source": "LEGORacers/src/race/racecameracontroller.cpp:18" + }, + { + "name": "shakePhaseScaleLow", + "value": 0.02, + "unit": "phase coefficient per millisecond", + "source": "LEGORacers/src/race/racecameracontroller.cpp:21" + }, + { + "name": "shakePhaseScaleHigh", + "value": 0.1, + "unit": "phase coefficient per millisecond", + "source": "LEGORacers/src/race/racecameracontroller.cpp:24" + }, + { + "name": "shakeRefreshPeriod", + "value": 500, + "unit": "milliseconds", + "source": "LEGORacers/src/race/racecameracontroller.cpp:236" + }, + { + "name": "profileTransitionDuration", + "value": 2000, + "unit": "milliseconds", + "source": "LEGORacers/src/race/racecameracontroller.cpp:534" + }, + { + "name": "transitionTurnRate", + "value": 0.0015707964, + "unit": "radians per millisecond", + "source": "LEGORacers/src/race/racecameracontroller.cpp:45" + } + ], + "sources": [ + { + "path": "LEGORacers/src/race/racecameracontroller.cpp", + "line": 223 + }, + { + "path": "LEGORacers/src/race/racecameracontroller.cpp", + "line": 534 + }, + { + "path": "LEGORacers/src/race/racecameracontroller.cpp", + "line": 726 + }, + { + "path": "LEGORacers/src/race/racecameracontroller.cpp", + "line": 103 + } + ], + "capabilities": [ + "fov shake", + "profile cross fade", + "audio listener sync" + ], + "tags": [ + "camera", + "feedback", + "speed", + "transition" + ] + }, + { + "id": "turbo-boost", + "node": "powerup-actions", + "name": "Turbo levels", + "summary": "Three turbo levels differ by how long the boost runs, not by how strong it starts", + "behavior": "A turbo is a state machine rather than a plain timer. Activating one allocates its effect entities, cancels any turbo already running on the car, and takes a level from zero to two. The ignite phase lasts 400 ms while the model plays its first animation part, then the boosting phase runs for the level's duration, and finally a 700 ms fade plays before the effect is torn down. Each activation also applies a negative pitch impulse of 0.0025 for 125 ms on the lowest level and 250 ms otherwise, so the chassis pitches visibly as the boost starts. Boost duration is the only number that scales with level: about one second, one and a half seconds and five seconds. A special fly cheat keeps the top level boosting indefinitely. The physics side of the turbo is a thrust override, a raised speed ceiling and a grip bonus, and the camera separately pulls in behind the car for the first three levels.", + "constants": [ + { + "name": "igniteDuration", + "value": 400, + "unit": "milliseconds", + "source": "LEGORacers/src/race/powerups/turboaction.cpp:124" + }, + { + "name": "fadeDuration", + "value": 700, + "unit": "milliseconds", + "source": "LEGORacers/src/race/powerups/turboaction.cpp:408" + }, + { + "name": "boostDurationLevel0", + "value": "0x03e8", + "unit": "milliseconds (1000)", + "source": "LEGORacers/include/race/powerups/turboaction.h:31" + }, + { + "name": "boostDurationLevel1", + "value": "0x05dc", + "unit": "milliseconds (1500)", + "source": "LEGORacers/include/race/powerups/turboaction.h:32" + }, + { + "name": "boostDurationLevel2", + "value": "0x1388", + "unit": "milliseconds (5000)", + "source": "LEGORacers/include/race/powerups/turboaction.h:34" + }, + { + "name": "pitchImpulseRate", + "value": -0.0025, + "unit": "radians per millisecond of applied pitch impulse", + "source": "LEGORacers/src/race/powerups/turboaction.cpp:215" + }, + { + "name": "shortPitchImpulseDuration", + "value": "0x007d", + "unit": "milliseconds (125)", + "source": "LEGORacers/include/race/powerups/turboaction.h:26" + }, + { + "name": "longPitchImpulseDuration", + "value": "0x00fa", + "unit": "milliseconds (250)", + "source": "LEGORacers/include/race/powerups/turboaction.h:27" + } + ], + "sources": [ + { + "path": "LEGORacers/src/race/powerups/turboaction.cpp", + "line": 91 + }, + { + "path": "LEGORacers/src/race/powerups/turboaction.cpp", + "line": 212 + }, + { + "path": "LEGORacers/src/race/powerups/turboaction.cpp", + "line": 363 + }, + { + "path": "LEGORacers/src/race/racer/racer.cpp", + "line": 2031 + } + ], + "capabilities": [ + "three turbo levels", + "staged effect state machine", + "pitch impulse" + ], + "tags": [ + "boost", + "item", + "state machine", + "feel" + ] + }, + { + "id": "shield-levels", + "node": "powerup-actions", + "name": "Shield levels", + "summary": "Four shield strengths from four to ten seconds, each with its own model, sound and contact reaction", + "behavior": "A shield is a timed aura with four levels. Activation first cancels any shield already on the car, then sets a timer from four seconds on the lowest level up through six and eight to ten seconds on the highest, starts a level-specific looping sound, and puts an outer and an inner shell around the car that follow its position and orientation with the up direction forced to world up. When the timer expires the state moves to a one second fade in which the alpha override is driven from the remaining milliseconds, and the shield is then removed with its end sound played five units above the car. The level decides the contact reaction: level one only shoves, level two adds a single turn spin, level three a double turn spin. Shielding also strips a curse from the car it is applied to, absorbs reaction hits without a penalty, and a level two or better shield returns a lobbed projectile to whoever fired it.", + "constants": [ + { + "name": "shieldDurationLevel0", + "value": "0x0fa0", + "unit": "milliseconds (4000)", + "source": "LEGORacers/include/race/powerups/shieldaction.h:26" + }, + { + "name": "shieldDurationLevel1", + "value": "0x1770", + "unit": "milliseconds (6000)", + "source": "LEGORacers/include/race/powerups/shieldaction.h:27" + }, + { + "name": "shieldDurationLevel2", + "value": "0x1f40", + "unit": "milliseconds (8000)", + "source": "LEGORacers/include/race/powerups/shieldaction.h:28" + }, + { + "name": "shieldDurationLevel3", + "value": "0x2710", + "unit": "milliseconds (10000)", + "source": "LEGORacers/include/race/powerups/shieldaction.h:29" + }, + { + "name": "shieldFadeDuration", + "value": 1000, + "unit": "milliseconds", + "source": "LEGORacers/src/race/powerups/shieldaction.cpp:273" + }, + { + "name": "shieldDrawLift", + "value": 5, + "unit": "world units above the car", + "source": "LEGORacers/src/race/powerups/shieldaction.cpp:283" + } + ], + "sources": [ + { + "path": "LEGORacers/src/race/powerups/shieldaction.cpp", + "line": 63 + }, + { + "path": "LEGORacers/src/race/powerups/shieldaction.cpp", + "line": 269 + }, + { + "path": "LEGORacers/src/race/powerups/shieldaction.cpp", + "line": 228 + }, + { + "path": "LEGORacers/src/race/powerups/racepowerupmanager.cpp", + "line": 1680 + } + ], + "capabilities": [ + "timed shield", + "level-based contact reaction", + "curse removal" + ], + "tags": [ + "item", + "defence", + "combat", + "duration" + ] + }, + { + "id": "projectile-ballistics", + "node": "powerup-actions", + "name": "Lobbed projectile solve", + "summary": "The lobbed projectile is given a flight time from distance over speed, then a vertical velocity that lands it on the predicted target", + "behavior": "Firing a lobbed projectile first finds a target point, or takes the car's own heading when no target is chosen, then predicts where the target will be by advancing it along its current velocity for the flight time. Flight time is horizontal distance divided by projectile speed, and the speed is nudged upward by the component of the shooter's own velocity pointing at the target, so firing while moving carries momentum into the shot. The trajectory solver then takes horizontal velocity from the delta over that duration and sets the vertical velocity to the height difference over the distance times speed, minus half of gravity times the distance over speed. That makes the arc land on the named target without integrating gravity error, which is why the shot reads as aimed rather than thrown. Each update re-evaluates position in closed form, horizontal travel being velocity times elapsed and the vertical term adding half of gravity times elapsed squared, so the flight is fully determined at launch. The projectile sweeps a segment against the world every step, expires when its lifetime runs out, and a shot that strikes a shield of level two or above is deflected back at whoever originally fired it, while an unshielded victim plays a hit reaction and drops a carried white brick.", + "constants": [ + { + "name": "cannonballSpeed", + "value": 180, + "unit": "world units per second", + "source": "LEGORacers/src/race/powerups/cannonballaction.cpp:43" + }, + { + "name": "cannonballGravity", + "value": -32.1759987, + "unit": "world units per second squared", + "source": "LEGORacers/src/race/powerups/cannonballaction.cpp:40" + }, + { + "name": "cannonballDefaultRange", + "value": 500, + "unit": "world units", + "source": "LEGORacers/src/race/powerups/cannonballaction.cpp:46" + }, + { + "name": "aimHeightOffset", + "value": 5, + "unit": "world units above the target origin", + "source": "LEGORacers/src/race/powerups/cannonballaction.cpp:25" + }, + { + "name": "minimumProjectileSpeed", + "value": 0.001, + "unit": "world units per second", + "source": "LEGORacers/src/race/powerups/powerupprojectile.cpp:15" + }, + { + "name": "gravityHalfFactor", + "value": 0.5, + "unit": "multiplier on gravity times elapsed squared", + "source": "LEGORacers/src/race/powerups/powerupprojectile.cpp:414" + }, + { + "name": "millisecondsPerSecond", + "value": 0.001, + "unit": "milliseconds to seconds", + "source": "LEGORacers/src/race/powerups/powerupprojectile.cpp:402" + } + ], + "sources": [ + { + "path": "LEGORacers/src/race/powerups/powerupprojectile.cpp", + "line": 180 + }, + { + "path": "LEGORacers/src/race/powerups/powerupprojectile.cpp", + "line": 332 + }, + { + "path": "LEGORacers/src/race/powerups/powerupprojectile.cpp", + "line": 387 + }, + { + "path": "LEGORacers/src/race/powerups/powerupprojectile.cpp", + "line": 289 + }, + { + "path": "LEGORacers/src/race/powerups/racepowerupmanager.cpp", + "line": 1380 + } + ], + "capabilities": [ + "target leading", + "closed-form ballistic arc", + "deflection" + ], + "tags": [ + "item", + "projectile", + "combat", + "physics" + ] + }, + { + "id": "homing-missile", + "node": "powerup-actions", + "name": "Homing missile pursuit", + "summary": "Missiles lock onto a racer in a cone, then follow the checkpoint chain until the target is close enough to chase directly", + "behavior": "A homing projectile acquires a target inside a cone, refreshed at most once a second, and refuses to consider its own owner, ghosts, shielded cars or cars already spun out. While the target is far away the missile does not fly straight at it: it walks the checkpoint chain from the owner's current checkpoint, skipping checkpoints whose plane it has already passed, and steers at a ground point found by probing 50 units down from the chosen checkpoint and lifting the resulting hit by 6. That keeps missiles roughly inside the track even when the target is around a corner. Once the target comes within the square root of 3600 units the missile switches to direct pursuit, keeps its speed and aims at a point five units above the car, and a square distance under 9 counts as a direct hit. A ground probe ahead of the missile keeps it from sinking into the floor, limiting descent to 6 units per second, and a growing spiral offset of 6 per second gives the path a visible wobble.", + "constants": [ + { + "name": "targetSnapDistanceSquared", + "value": 3600, + "unit": "squared world units (60 units)", + "source": "LEGORacers/src/race/powerups/homingprojectile.cpp:18" + }, + { + "name": "directHitDistanceSquared", + "value": 9, + "unit": "squared world units (3 units)", + "source": "LEGORacers/src/race/powerups/homingprojectile.cpp:21" + }, + { + "name": "descentRateLimit", + "value": 6, + "unit": "world units per second", + "source": "LEGORacers/src/race/powerups/homingprojectile.cpp:30" + }, + { + "name": "spiralGrowth", + "value": 6, + "unit": "world units per second", + "source": "LEGORacers/src/race/powerups/homingprojectile.cpp:33" + }, + { + "name": "pathSkipDistanceSquared", + "value": 90000, + "unit": "squared world units (300 units)", + "source": "LEGORacers/src/race/powerups/homingprojectile.cpp:36" + }, + { + "name": "groundProbeDepth", + "value": 50, + "unit": "world units", + "source": "LEGORacers/src/race/powerups/homingprojectile.cpp:39" + }, + { + "name": "targetRefreshPeriod", + "value": 1000, + "unit": "milliseconds", + "source": "LEGORacers/include/race/powerups/homingprojectile.h:33" + }, + { + "name": "groundProbeStartOffset", + "value": 2, + "unit": "world units above the missile", + "source": "LEGORacers/src/race/powerups/homingprojectile.cpp:24" + }, + { + "name": "collisionHitLift", + "value": 6, + "unit": "world units above the surface", + "source": "LEGORacers/src/race/powerups/homingprojectile.cpp:27" + } + ], + "sources": [ + { + "path": "LEGORacers/src/race/powerups/homingprojectile.cpp", + "line": 81 + }, + { + "path": "LEGORacers/src/race/powerups/homingprojectile.cpp", + "line": 164 + }, + { + "path": "LEGORacers/src/race/powerups/homingprojectile.cpp", + "line": 200 + }, + { + "path": "LEGORacers/src/race/powerups/homingprojectile.cpp", + "line": 339 + } + ], + "capabilities": [ + "cone target acquisition", + "waypoint following", + "spiral approach" + ], + "tags": [ + "item", + "projectile", + "combat", + "ai" + ] + }, + { + "id": "brick-charge", + "node": "powerup-bricks", + "name": "Brick pickups and powerup levels", + "summary": "A coloured brick selects a powerup family, white bricks raise the level, and only four families exist", + "behavior": "Pickups are ordinary bricks lying on the track that grow from zero scale over 400 ms, settle back to 0.8 scale, and become touchable half a second later. Touching one while the car holds no brick starts a 250 ms transition, then the brick respawns after 3000 ms; touching one while a brick is already held swaps the colours instead and leaves that brick active, which is how a player trades a family for another. A ghost car cannot collect at all, and a car that has already touched a brick in this pass is not offered the same one twice. The colour of the held brick chooses the family: red gives a lobbed shot, a grappling hook, a lightning strike and homing missiles as the level rises; yellow gives an oil slick, thrown dynamite, a magnet and a curse; blue gives shields; green gives turbo, with the top green level instead warping the car forward along the track. The level index comes from how many white bricks the car is carrying, capped at three, and firing consumes the held brick and returns every white brick, so charging is a deliberate detour.", + "constants": [ + { + "name": "brickRespawnDelay", + "value": 3000, + "unit": "milliseconds", + "source": "LEGORacers/src/race/powerups/pickupbrick.cpp:53" + }, + { + "name": "brickGrowDuration", + "value": 400, + "unit": "milliseconds", + "source": "LEGORacers/src/race/powerups/pickupbrick.cpp:106" + }, + { + "name": "brickSettleRate", + "value": 0.002, + "unit": "scale units per millisecond", + "source": "LEGORacers/src/race/powerups/pickupbrick.cpp:19" + }, + { + "name": "brickRestingScale", + "value": 0.8, + "unit": "model scale", + "source": "LEGORacers/src/race/powerups/pickupbrick.cpp:22" + }, + { + "name": "brickSwapTransition", + "value": 250, + "unit": "milliseconds", + "source": "LEGORacers/src/race/powerups/colorbrick.cpp:68" + }, + { + "name": "brickTouchableDelay", + "value": 500, + "unit": "milliseconds", + "source": "LEGORacers/src/race/powerups/colorbrick.cpp:82" + }, + { + "name": "droppedBrickReturnDelay", + "value": 10000, + "unit": "milliseconds", + "source": "LEGORacers/src/race/powerups/whitebrick.cpp:60" + }, + { + "name": "maximumWhiteBricks", + "value": 3, + "unit": "bricks carried (level index ceiling)", + "source": "LEGORacers/include/race/racer/racer.h:258" + } + ], + "sources": [ + { + "path": "LEGORacers/src/race/powerups/pickupbrick.cpp", + "line": 95 + }, + { + "path": "LEGORacers/src/race/powerups/pickupbrick.cpp", + "line": 142 + }, + { + "path": "LEGORacers/src/race/powerups/colorbrick.cpp", + "line": 110 + }, + { + "path": "LEGORacers/src/race/powerups/whitebrick.cpp", + "line": 50 + }, + { + "path": "LEGORacers/src/race/powerups/racepowerupmanager.cpp", + "line": 1336 + }, + { + "path": "LEGORacers/src/race/powerups/racepowerupmanager.cpp", + "line": 1770 + } + ], + "capabilities": [ + "colour-coded powerups", + "charging levels", + "pickup respawn" + ], + "tags": [ + "item", + "pickup", + "progression", + "hud" + ] + }, + { + "id": "lap-detection", + "node": "race-progress", + "name": "Lap validation and checkpoint progress", + "summary": "A lap only counts when the car passes through three ordered zones, and progress is a checkpoint count plus a fraction", + "behavior": "Track progress is measured on an oriented graph of checkpoint planes, each carrying a position, a plane normal and a fraction of the lap. A crossing is compared against the normal of the surface the car actually crossed, which resolves the direction of travel: a forward crossing of the lap boundary increments the checkpoint count unless the car is still flagged as having gone backwards, and a reversed crossing at the boundary zeroes the zone history and flags the car so that the next ordinary checkpoint pays the count back. Progress for standings is then the checkpoint count plus the fraction of the plane the car is heading for, so two cars between the same checkpoints are separated smoothly instead of sharing a rank. The finish line itself is handled by three overlapping zones: open track, the line zone and a post-line zone. A lap increments only when the car arrives at the line having come through the post-line zone with the line zone before it, so reversing across the line, cutting out and back, or parking on it cannot bank laps. When a car reaches the lap count the session marks it finished, awards cup points if a cup is running and switches that player's camera to the victory view. A countdown of 3000 ms precedes the race, and a car that is behind on time with laps still to run shows the gap for 2000 ms.", + "constants": [ + { + "name": "openTrackZone", + "value": 0, + "unit": "zone index", + "source": "LEGORacers/src/race/events/lapzoneresource.cpp:53" + }, + { + "name": "finishLineZone", + "value": 1, + "unit": "zone index", + "source": "LEGORacers/src/race/events/lapzoneresource.cpp:56" + }, + { + "name": "postLineZone", + "value": 2, + "unit": "zone index", + "source": "LEGORacers/src/race/events/lapzoneresource.cpp:59" + }, + { + "name": "unsetLapFraction", + "value": -1, + "unit": "lap fraction", + "source": "LEGORacers/src/race/checkpointgraph.cpp:32" + }, + { + "name": "unsetNextIndex", + "value": "0xffffffff", + "unit": "checkpoint index sentinel", + "source": "LEGORacers/src/race/checkpointgraph.cpp:24" + }, + { + "name": "countdownDuration", + "value": 3000, + "unit": "milliseconds", + "source": "LEGORacers/src/race/racesession.cpp:1776" + }, + { + "name": "timeGapDisplayDuration", + "value": 2000, + "unit": "milliseconds", + "source": "LEGORacers/src/race/racer/racer.cpp:2269" + }, + { + "name": "facingForwardHold", + "value": 500, + "unit": "milliseconds", + "source": "LEGORacers/src/race/racer/racer.cpp:2374" + } + ], + "sources": [ + { + "path": "LEGORacers/src/race/events/lapzoneresource.cpp", + "line": 50 + }, + { + "path": "LEGORacers/src/race/racer/racer.cpp", + "line": 2250 + }, + { + "path": "LEGORacers/src/race/racer/racer.cpp", + "line": 2435 + }, + { + "path": "LEGORacers/src/race/racer/racer.cpp", + "line": 2484 + }, + { + "path": "LEGORacers/src/race/checkpointgraphwalk.cpp", + "line": 6 + }, + { + "path": "LEGORacers/src/race/racestate.cpp", + "line": 637 + } + ], + "capabilities": [ + "zone-gated lap counting", + "directed checkpoint planes", + "progress ranking" + ], + "tags": [ + "race", + "progress", + "checkpoints", + "rules" + ] + }, + { + "id": "route-playback", + "node": "route-motion", + "name": "Recorded route playback", + "summary": "Path-driven racers advance along a recorded trajectory at a scaled playback speed with ramps, lateral drift and a jump arc", + "behavior": "Some cars do not simulate forces at all; they advance a cursor along a recorded route. Playback speed is a scalar where 1.0 means the recorded timing, driven toward a target with separate acceleration and deceleration rates, with faster rates available while a boost or an external push is active. During the first two seconds of a boost the target ramps from 1.7 up to 2.75, and ending the boost returns it to 1. A curse drops the target to 0.5 and a ghost mode sets 4.5. Impulse-driven speed changes are clamped between -0.5 and 2.75, so a car can be pushed slightly backwards along its own route but never far. A lateral offset from the centre of the path decays back to zero at a fixed rate, and the slide state bends the path visually by sampling the route half a second ahead and comparing right axes, which gives cornering lean without any steering. Route jumps are a small ballistic arc under a gravity of -100 that clears the spin-out state when the car lands. Wall contact simply reverses playback slightly and rewinds the step, and large directional impulses from hits are converted into playback changes capped at 240 units.", + "constants": [ + { + "name": "nominalPlaybackSpeed", + "value": 1, + "unit": "playback speed multiplier", + "source": "LEGORacers/src/race/racer/racerphysics.cpp:211" + }, + { + "name": "boostPlaybackSpeed", + "value": 1.7, + "unit": "playback speed multiplier", + "source": "LEGORacers/src/race/racer/racerphysics.cpp:100" + }, + { + "name": "ghostPlaybackSpeed", + "value": 4.5, + "unit": "playback speed multiplier", + "source": "LEGORacers/src/race/racer/racerphysics.cpp:103" + }, + { + "name": "cursePlaybackSpeed", + "value": 0.5, + "unit": "playback speed multiplier", + "source": "LEGORacers/src/race/racer/racerphysics.cpp:106" + }, + { + "name": "maximumPlaybackSpeed", + "value": 2.75, + "unit": "playback speed multiplier", + "source": "LEGORacers/src/race/racer/racerphysics.cpp:109" + }, + { + "name": "minimumPlaybackSpeed", + "value": -0.5, + "unit": "playback speed multiplier", + "source": "LEGORacers/src/race/racer/racerphysics.cpp:112" + }, + { + "name": "playbackAcceleration", + "value": 0.00048828125, + "unit": "playback speed per millisecond", + "source": "LEGORacers/src/race/racer/racerphysics.cpp:130" + }, + { + "name": "playbackDeceleration", + "value": 0.0020000001, + "unit": "playback speed per millisecond", + "source": "LEGORacers/src/race/racer/racerphysics.cpp:127" + }, + { + "name": "boostPlaybackAcceleration", + "value": 0.0099999998, + "unit": "playback speed per millisecond", + "source": "LEGORacers/src/race/racer/racerphysics.cpp:133" + }, + { + "name": "sideOffsetDecay", + "value": 0.001953125, + "unit": "world units per millisecond", + "source": "LEGORacers/src/race/racer/racerphysics.cpp:142" + }, + { + "name": "boostRampWindow", + "value": 2000, + "unit": "milliseconds", + "source": "LEGORacers/src/race/racer/racerphysics.cpp:157" + }, + { + "name": "routeWallBackSpeed", + "value": -0.1, + "unit": "playback speed multiplier", + "source": "LEGORacers/src/race/racer/racerphysics.cpp:94" + }, + { + "name": "maximumDirectionalImpulse", + "value": 240, + "unit": "impulse units", + "source": "LEGORacers/src/race/racer/racerphysics.cpp:124" + } + ], + "sources": [ + { + "path": "LEGORacers/src/race/racer/racerphysics.cpp", + "line": 748 + }, + { + "path": "LEGORacers/src/race/racer/racerphysics.cpp", + "line": 446 + }, + { + "path": "LEGORacers/src/race/racer/racerphysics.cpp", + "line": 1036 + }, + { + "path": "LEGORacers/src/race/racer/routecursor.cpp", + "line": 127 + }, + { + "path": "LEGORacers/src/race/racer/racer.cpp", + "line": 730 + } + ], + "capabilities": [ + "path following", + "speed ramps", + "lateral drift", + "jump arc" + ], + "tags": [ + "ai", + "movement", + "route", + "ghost" + ] + }, + { + "id": "steering-input", + "node": "player-input", + "name": "Analogue shaped steering input", + "summary": "Digital left and right keys ramp a virtual stick at a fixed rate, and the drift button changes both the rate and the return", + "behavior": "Input is not passed straight to the car. The player layer keeps a virtual steering value between minus one and one and moves it toward the requested direction every millisecond: 1.25 per second when a direction key is held, 8.25 per second when the value has to come back through zero to the other side, and 5 or 16 per second while drifting, the faster figure used while sliding one way and the slower the other. With no input the value decays at the idle rate of 2.5 per second. Holding both directions, or centring the value, pins the limits so the value cannot cross zero by accident. An analogue axis overrides the key ramp: its deflection sets the limit while the same ramp smooths the approach. The resulting value is multiplied by the car handling scale and handed to the drive controller as a curvature request, which is why keyboard steering on this engine feels progressive instead of binary.", + "constants": [ + { + "name": "idleSteerRate", + "value": 2.5, + "unit": "steering units per second", + "source": "LEGORacers/src/race/playercontrols.cpp:15" + }, + { + "name": "inputSteerRate", + "value": 1.25, + "unit": "steering units per second", + "source": "LEGORacers/src/race/playercontrols.cpp:21" + }, + { + "name": "steerReturnRate", + "value": 8.25, + "unit": "steering units per second", + "source": "LEGORacers/src/race/playercontrols.cpp:24" + }, + { + "name": "driftSteerRate", + "value": 5, + "unit": "steering units per second", + "source": "LEGORacers/src/race/playercontrols.cpp:27" + }, + { + "name": "driftOpposingSteerRate", + "value": 16, + "unit": "steering units per second", + "source": "LEGORacers/src/race/playercontrols.cpp:30" + }, + { + "name": "steeringLimit", + "value": 1, + "unit": "virtual steering units", + "source": "LEGORacers/src/race/playercontrols.cpp:33" + } + ], + "sources": [ + { + "path": "LEGORacers/src/race/playercontrols.cpp", + "line": 84 + }, + { + "path": "LEGORacers/src/race/playercontrols.cpp", + "line": 213 + }, + { + "path": "LEGORacers/src/race/playercontrols.cpp", + "line": 289 + }, + { + "path": "LEGORacers/src/race/racer/drivecontroller.cpp", + "line": 220 + } + ], + "capabilities": [ + "digital to analogue shaping", + "drift-aware steering rates", + "input smoothing" + ], + "tags": [ + "controls", + "steering", + "feel", + "input" + ] + }, + { + "id": "hold-release-boost", + "node": "player-input", + "name": "Pre-start boost window", + "summary": "A 100 ms turbo window opens at the lights and is consumed by a throttle press that lands inside it", + "behavior": "The boost window is a small state machine that only arms around the start of a race. A throttle hold longer than 2000 ms while the pre-start flag is still set opens a window of 100 ms and resets the hold timer. At the start the session asks every player's controls to make one attempt: if a window is still open it fires the green family straight away, choosing the strongest level when at least 60 ms remain and the weakest otherwise, and clears the window; if no window is open and the brake is not held it arms a fresh window of the same length. Because the attempt clears the pre-start flag, a long hold later in the race cannot re-arm anything, but any throttle press that lands while a window is still open runs the same attempt and also spawns a puff of smoke from the car. In practice that makes the start a timing test on a 100 ms window: a press in the first 40 ms of it pays the strongest level, a press in the last 60 ms pays the weakest, and a player who never has a window open gets nothing.", + "constants": [ + { + "name": "throttleHoldRequirement", + "value": 2000, + "unit": "milliseconds of continuous throttle", + "source": "LEGORacers/src/race/playercontrols.cpp:455" + }, + { + "name": "boostWindowLength", + "value": 100, + "unit": "milliseconds", + "source": "LEGORacers/src/race/playercontrols.cpp:434" + }, + { + "name": "strongBoostWindowThreshold", + "value": 60, + "unit": "milliseconds of window remaining", + "source": "LEGORacers/src/race/playercontrols.cpp:425" + } + ], + "sources": [ + { + "path": "LEGORacers/src/race/playercontrols.cpp", + "line": 418 + }, + { + "path": "LEGORacers/src/race/playercontrols.cpp", + "line": 451 + }, + { + "path": "LEGORacers/src/race/racesession.cpp", + "line": 1786 + } + ], + "capabilities": [ + "timed input window", + "free turbo", + "start bonus" + ], + "tags": [ + "boost", + "timing", + "controls", + "technique" + ] + }, + { + "id": "moving-obstacle", + "node": "hazards", + "name": "Animated obstacle with impact windows", + "summary": "A moving crane carries a trigger sphere, and its swing only damages racers during two windows of its animation", + "behavior": "Hazard objects are loaded per track and activated by event ids from the event table, so a track can switch a hazard on and off as the race passes through it. This one rides an animated track piece: every update the entity's local origin is transformed up through its parent chain, scaled by the model and entity scale, converted to world space and handed to a trigger body whose radius is 3, so the collision volume follows the animation exactly rather than being baked into geometry. Damage is not fired on contact but on animation frames: a pending flag is set while the part time sits between 60 and 120 ms or between 210 and 270 ms, and the actual impact event fires when the part time then passes the 0 and 30 ms window or the 150 to 180 ms window, after which the flag has to rearm. That means the visual swing telegraphs the hit a fraction of a second before the trigger becomes live. A looping sound is anchored to the moving crane with a maximum audible distance of 300 units, and the update also keeps a projected shadow decal under it, so the player can read the swing from a distance.", + "constants": [ + { + "name": "obstacleTriggerRadius", + "value": 3, + "unit": "world units", + "source": "LEGORacers/src/race/hazards/movingobstaclehazard.cpp:26" + }, + { + "name": "obstacleAudibleDistance", + "value": 300, + "unit": "world units", + "source": "LEGORacers/src/race/hazards/movingobstaclehazard.cpp:23" + }, + { + "name": "impactWindowStart", + "value": 0, + "unit": "milliseconds of part time", + "source": "LEGORacers/src/race/hazards/movingobstaclehazard.cpp:35" + }, + { + "name": "impactWindowEnd", + "value": 30, + "unit": "milliseconds of part time", + "source": "LEGORacers/src/race/hazards/movingobstaclehazard.cpp:38" + }, + { + "name": "impactWindowLateStart", + "value": 150, + "unit": "milliseconds of part time", + "source": "LEGORacers/src/race/hazards/movingobstaclehazard.cpp:29" + }, + { + "name": "impactWindowLateEnd", + "value": 180, + "unit": "milliseconds of part time", + "source": "LEGORacers/src/race/hazards/movingobstaclehazard.cpp:32" + }, + { + "name": "rearmWindowStart", + "value": 60, + "unit": "milliseconds of part time", + "source": "LEGORacers/src/race/hazards/movingobstaclehazard.cpp:41" + }, + { + "name": "rearmWindowEnd", + "value": 120, + "unit": "milliseconds of part time", + "source": "LEGORacers/src/race/hazards/movingobstaclehazard.cpp:44" + }, + { + "name": "rearmWindowSecondStart", + "value": 210, + "unit": "milliseconds of part time", + "source": "LEGORacers/src/race/hazards/movingobstaclehazard.cpp:47" + }, + { + "name": "rearmWindowSecondEnd", + "value": 270, + "unit": "milliseconds of part time", + "source": "LEGORacers/src/race/hazards/movingobstaclehazard.cpp:50" + } + ], + "sources": [ + { + "path": "LEGORacers/src/race/hazards/movingobstaclehazard.cpp", + "line": 151 + }, + { + "path": "LEGORacers/src/race/hazards/movingobstaclehazard.cpp", + "line": 109 + }, + { + "path": "LEGORacers/src/race/hazards/movingobstaclehazard.cpp", + "line": 200 + }, + { + "path": "LEGORacers/src/race/hazardmanager.cpp", + "line": 305 + } + ], + "capabilities": [ + "moving trigger volume", + "animation-synced damage", + "event-driven activation" + ], + "tags": [ + "hazard", + "trigger", + "animation", + "track" + ] + }, + { + "id": "fixed-substep", + "node": "race-loop", + "name": "Frame pacing and fixed substep", + "summary": "The frame delta is capped, then sliced into 40 ms simulation steps that drive racers and cameras together", + "behavior": "The application clamps the measured frame delta to 100 ms so a stall cannot teleport the world, and sleeps to enforce a floor of 12 ms, which caps the frame rate near 83 Hz. The race session takes that delta and slices it: while time remains it takes a step of at most 40 ms and runs racer physics and every camera with that step, so the simulation advances at 25 Hz regardless of frame rate while rendering still runs at frame rate. Input shaping, force feedback and the demo text timer run once per frame before the slicing. Inside a step the order is fixed and observable: drive control for each racer, then physics for each racer, then drift lean, then roster bookkeeping, then car animation, then engine and spatial sounds, then timers, then standings. Hazards, event tables, particle animations, decals and audio all advance on the unsliced frame delta before the loop, which is why particles stay smooth while the physics marches in discrete steps. The countdown state holds until 3000 ms have elapsed, then starts the race, asks every player's controls to make a boost attempt and switches the music, which is the one place input is sampled on a state boundary rather than per step.", + "constants": [ + { + "name": "simulationSubstep", + "value": 40, + "unit": "milliseconds per fixed step", + "source": "LEGORacers/include/race/racesession.h:116" + }, + { + "name": "maximumFrameDelta", + "value": 100, + "unit": "milliseconds", + "source": "LEGORacers/src/app/golapp.cpp:11" + }, + { + "name": "minimumFrameDelta", + "value": 12, + "unit": "milliseconds", + "source": "LEGORacers/include/app/win32golapp.h:63" + } + ], + "sources": [ + { + "path": "LEGORacers/src/race/racesession.cpp", + "line": 2032 + }, + { + "path": "LEGORacers/src/race/racesession.cpp", + "line": 2076 + }, + { + "path": "LEGORacers/src/race/racestate.cpp", + "line": 587 + }, + { + "path": "LEGORacers/src/app/win32golapp.cpp", + "line": 430 + }, + { + "path": "LEGORacers/src/race/racesession.cpp", + "line": 1774 + } + ], + "capabilities": [ + "fixed timestep", + "frame delta clamp", + "deterministic update order" + ], + "tags": [ + "engine", + "timestep", + "loop", + "determinism" + ] + } + ] +} diff --git a/data/games/librecorsa.json b/data/games/librecorsa.json new file mode 100644 index 0000000..4e27d9e --- /dev/null +++ b/data/games/librecorsa.json @@ -0,0 +1,2839 @@ +{ + "version": 1, + "id": "librecorsa", + "title": "LibreCorsa", + "repo": "https://github.com/KhazP/librecorsa", + "sha": "b6bd7a419d5f4c12f34254a79e5aec05c74d7867", + "nodes": [ + { + "id": "vehicle-dynamics", + "name": "Rigid-body car core", + "purpose": "One rigid body with four independently suspended wheels, integrated in fixed steps against the scanned circuit surface, with barrier, floor and plank contacts resolved inside the same step", + "paths": [ + "src/lib/sim/f12026-car/VehiclePhysics.ts", + "src/lib/f1-physics-2026.ts" + ], + "functions": [ + { + "name": "step", + "path": "src/lib/sim/f12026-car/VehiclePhysics.ts", + "lines": [ + 1157, + 1165 + ] + }, + { + "name": "outerStep", + "path": "src/lib/sim/f12026-car/VehiclePhysics.ts", + "lines": [ + 1420, + 1443 + ] + }, + { + "name": "substep", + "path": "src/lib/sim/f12026-car/VehiclePhysics.ts", + "lines": [ + 1459, + 1817 + ] + }, + { + "name": "applyPlankContact", + "path": "src/lib/sim/f12026-car/VehiclePhysics.ts", + "lines": [ + 2300, + 2322 + ] + }, + { + "name": "step", + "path": "src/lib/f1-physics-2026.ts", + "lines": [ + 994, + 1060 + ] + } + ], + "dependsOn": [ + "tyre-model", + "suspension", + "aero", + "drivetrain", + "brakes-electronics" + ] + }, + { + "id": "tyre-model", + "name": "Tyre contact model", + "purpose": "Slip-curve and combined-slip force generation, the five-point contact patch, a nine-node thermal network with inflation pressure, wear and damage states, and the wet-road grip reduction", + "paths": [ + "src/lib/sim/ac-tyre.ts", + "src/lib/sim/wet-tyres.ts", + "src/lib/sim/f12026-car/F12026CarConfig.ts" + ], + "functions": [ + { + "name": "computeTyreForces", + "path": "src/lib/sim/ac-tyre.ts", + "lines": [ + 533, + 716 + ] + }, + { + "name": "updateTyreThermal", + "path": "src/lib/sim/ac-tyre.ts", + "lines": [ + 860, + 948 + ] + }, + { + "name": "stepFlatSpot", + "path": "src/lib/sim/ac-tyre.ts", + "lines": [ + 723, + 765 + ] + }, + { + "name": "stepGrainBlister", + "path": "src/lib/sim/ac-tyre.ts", + "lines": [ + 773, + 795 + ] + }, + { + "name": "advanceDirtyLevel", + "path": "src/lib/sim/ac-tyre.ts", + "lines": [ + 818, + 837 + ] + }, + { + "name": "wetGripFactor", + "path": "src/lib/sim/wet-tyres.ts", + "lines": [ + 140, + 148 + ] + }, + { + "name": "compoundData", + "path": "src/lib/sim/f12026-car/F12026CarConfig.ts", + "lines": [ + 586, + 596 + ] + } + ], + "dependsOn": [] + }, + { + "id": "suspension", + "name": "Double-wishbone kinematics", + "purpose": "Solves the live three-dimensional wishbone, upright and tie-rod geometry from wheel travel, and turns it into camber gain, bump steer, caster and scrub radius, roll-centre migration and anti-dive", + "paths": [ + "src/lib/sim/suspension-kinematics.ts" + ], + "functions": [ + { + "name": "solveCornerKinematics3D", + "path": "src/lib/sim/suspension-kinematics.ts", + "lines": [ + 284, + 454 + ] + }, + { + "name": "solveAxleKinematics3D", + "path": "src/lib/sim/suspension-kinematics.ts", + "lines": [ + 461, + 492 + ] + } + ], + "dependsOn": [] + }, + { + "id": "aero", + "name": "Aerodynamic platform", + "purpose": "Wing and floor loads from ride height and attitude, drag, the wake a car sheds on the cars behind it, ground-effect loss in roll, and the relative airspeed the bodywork actually sees", + "paths": [ + "src/lib/sim/aero-wake.ts", + "src/lib/sim/f12026-car/VehiclePhysics.ts" + ], + "functions": [ + { + "name": "evaluateAeroWakeDeficit", + "path": "src/lib/sim/aero-wake.ts", + "lines": [ + 63, + 142 + ] + }, + { + "name": "evaluateRollAeroFactor", + "path": "src/lib/sim/aero-wake.ts", + "lines": [ + 149, + 152 + ] + }, + { + "name": "computeRelativeAirspeed", + "path": "src/lib/sim/aero-wake.ts", + "lines": [ + 169, + 194 + ] + }, + { + "name": "applyMapAero", + "path": "src/lib/sim/f12026-car/VehiclePhysics.ts", + "lines": [ + 2232, + 2297 + ] + } + ], + "dependsOn": [ + "vehicle-dynamics" + ] + }, + { + "id": "drivetrain", + "name": "Engine, energy recovery and differential", + "purpose": "Combustion torque with turbo lag and engine maps, clutch take-up, the electric machine deploy and harvest budget, the gearbox schedule and the limited-slip differential solved as sequential impulses", + "paths": [ + "src/lib/sim/f12026-car/VehiclePhysics.ts", + "src/lib/sim/f12026-car/F12026CarConfig.ts" + ], + "functions": [ + { + "name": "solveDrivetrain", + "path": "src/lib/sim/f12026-car/VehiclePhysics.ts", + "lines": [ + 1929, + 2150 + ] + }, + { + "name": "updateDrs", + "path": "src/lib/sim/f12026-car/VehiclePhysics.ts", + "lines": [ + 1370, + 1414 + ] + }, + { + "name": "autoShiftFor", + "path": "src/lib/sim/f12026-car/VehiclePhysics.ts", + "lines": [ + 1337, + 1369 + ] + } + ], + "dependsOn": [ + "vehicle-dynamics" + ] + }, + { + "id": "brakes-electronics", + "name": "Brakes and driver aids", + "purpose": "Two-node brake disc thermal network with temperature-dependent pad friction, brake-by-wire bias migration, and the multi-stage traction control and per-wheel antilock system of each car", + "paths": [ + "src/lib/sim/brake-physics.ts", + "src/lib/sim/f12026-car/F12026CarSim.ts" + ], + "functions": [ + { + "name": "evaluateBrakePadMuFactor", + "path": "src/lib/sim/brake-physics.ts", + "lines": [ + 69, + 91 + ] + }, + { + "name": "stepBrakeDiscThermal", + "path": "src/lib/sim/brake-physics.ts", + "lines": [ + 105, + 162 + ] + }, + { + "name": "computeDynamicBrakeMigration", + "path": "src/lib/sim/brake-physics.ts", + "lines": [ + 172, + 181 + ] + }, + { + "name": "driverCommand", + "path": "src/lib/sim/f12026-car/F12026CarSim.ts", + "lines": [ + 1112, + 1226 + ] + } + ], + "dependsOn": [ + "tyre-model" + ] + }, + { + "id": "surfaces", + "name": "Track surface table", + "purpose": "The per-surface data table the contact query reads: dry and wet friction, rolling resistance, a sinusoidal ripple with a real wavelength, vibration gain and whether a surface counts for track limits, plus the kerb cross-section", + "paths": [ + "src/lib/sim/track-surfaces.ts", + "src/lib/sim/f12026-car/TrackColliderQuery.ts" + ], + "functions": [ + { + "name": "resolveSurfaceTable", + "path": "src/lib/sim/track-surfaces.ts", + "lines": [ + 53, + 68 + ] + }, + { + "name": "kerbProfileM", + "path": "src/lib/sim/track-surfaces.ts", + "lines": [ + 79, + 87 + ] + }, + { + "name": "surfaceRippleM", + "path": "src/lib/sim/track-surfaces.ts", + "lines": [ + 93, + 96 + ] + }, + { + "name": "wallSweep", + "path": "src/lib/sim/f12026-car/TrackColliderQuery.ts", + "lines": [ + 781, + 820 + ] + } + ], + "dependsOn": [] + }, + { + "id": "weather", + "name": "Weather and standing water", + "purpose": "Sun and cloud, the water film that rain lays down and that drains and evaporates, the share of it the racing line keeps, and the tyre-side grip loss that follows from film depth and speed", + "paths": [ + "src/lib/sim/weather.ts", + "src/lib/sim/wet-tyres.ts" + ], + "functions": [ + { + "name": "aquaplaneFactor", + "path": "src/lib/sim/wet-tyres.ts", + "lines": [ + 126, + 134 + ] + }, + { + "name": "wetPneumaticTrailScale", + "path": "src/lib/sim/wet-tyres.ts", + "lines": [ + 155, + 160 + ] + }, + { + "name": "hydrodynamicDragN", + "path": "src/lib/sim/wet-tyres.ts", + "lines": [ + 167, + 173 + ] + }, + { + "name": "step", + "path": "src/lib/sim/weather.ts", + "lines": [ + 664, + 731 + ] + }, + { + "name": "depositCarPass", + "path": "src/lib/sim/weather.ts", + "lines": [ + 739, + 746 + ] + } + ], + "dependsOn": [ + "surfaces" + ] + }, + { + "id": "track-state", + "name": "Evolving track and car contacts", + "purpose": "Per-sector rubber and marble state with a grip multiplier, rain washing the line back toward green, and the rigid-body oriented-box contact solver used when two cars touch", + "paths": [ + "src/lib/sim/track-evolution.ts" + ], + "functions": [ + { + "name": "depositCarPass", + "path": "src/lib/sim/track-evolution.ts", + "lines": [ + 131, + 171 + ] + }, + { + "name": "weatherWash", + "path": "src/lib/sim/track-evolution.ts", + "lines": [ + 177, + 186 + ] + }, + { + "name": "sample", + "path": "src/lib/sim/track-evolution.ts", + "lines": [ + 193, + 239 + ] + }, + { + "name": "resolveCarToCarObbCollision", + "path": "src/lib/sim/track-evolution.ts", + "lines": [ + 341, + 500 + ] + } + ], + "dependsOn": [ + "surfaces" + ] + }, + { + "id": "ai-field", + "name": "Opponent field", + "purpose": "The AI grid: a per-car target speed profile with backward braking passes, skill anchors that change pace and error size, traffic planning for following, passing and defending, section learning and the start procedure", + "paths": [ + "src/lib/sim/ai-opponents.ts", + "src/lib/sim/ai-tuning.ts", + "src/lib/sim/ai-learning.ts" + ], + "functions": [ + { + "name": "update", + "path": "src/lib/sim/ai-opponents.ts", + "lines": [ + 650, + 677 + ] + }, + { + "name": "planTraffic", + "path": "src/lib/sim/ai-opponents.ts", + "lines": [ + 724, + 849 + ] + }, + { + "name": "resolveContacts", + "path": "src/lib/sim/ai-opponents.ts", + "lines": [ + 855, + 899 + ] + }, + { + "name": "decelEnvelope", + "path": "src/lib/sim/ai-tuning.ts", + "lines": [ + 424, + 442 + ] + }, + { + "name": "buildTargetSpeedProfile", + "path": "src/lib/sim/ai-tuning.ts", + "lines": [ + 456, + 493 + ] + }, + { + "name": "aiSkillModifiers", + "path": "src/lib/sim/ai-tuning.ts", + "lines": [ + 245, + 255 + ] + }, + { + "name": "observe", + "path": "src/lib/sim/ai-learning.ts", + "lines": [ + 107, + 168 + ] + } + ], + "dependsOn": [ + "vehicle-dynamics" + ] + }, + { + "id": "driver-input", + "name": "Driver input, camera and force feedback", + "purpose": "Stick steering assist for pad drivers, the cockpit micro-shake that reports what the suspension is doing, the automatic trackside camera director, and the wheel force-feedback path from steering torque to rim force", + "paths": [ + "src/lib/sim/gamepad-assist.ts", + "src/lib/render/camera-rig.ts", + "src/lib/input/wheel-ffb.ts", + "src/lib/input/webhid-ffb.ts" + ], + "functions": [ + { + "name": "update", + "path": "src/lib/sim/gamepad-assist.ts", + "lines": [ + 152, + 227 + ] + }, + { + "name": "update", + "path": "src/lib/render/camera-rig.ts", + "lines": [ + 70, + 158 + ] + }, + { + "name": "update", + "path": "src/lib/render/camera-rig.ts", + "lines": [ + 405, + 469 + ] + }, + { + "name": "shapeFfbForce", + "path": "src/lib/input/wheel-ffb.ts", + "lines": [ + 39, + 53 + ] + }, + { + "name": "softLockForce", + "path": "src/lib/input/wheel-ffb.ts", + "lines": [ + 55, + 60 + ] + } + ], + "dependsOn": [] + }, + { + "id": "race-control", + "name": "Timing, classification and track limits", + "purpose": "Race distance and lap clocks, gap and split timing, the final classification, and the referee that judges excursions, advantage gained and the penalties that follow", + "paths": [ + "src/lib/sim/race-session.ts", + "src/lib/sim/track-limits.ts" + ], + "functions": [ + { + "name": "classify", + "path": "src/lib/sim/race-session.ts", + "lines": [ + 198, + 225 + ] + }, + { + "name": "update", + "path": "src/lib/sim/race-session.ts", + "lines": [ + 259, + 277 + ] + }, + { + "name": "update", + "path": "src/lib/sim/track-limits.ts", + "lines": [ + 153, + 208 + ] + }, + { + "name": "strike", + "path": "src/lib/sim/track-limits.ts", + "lines": [ + 327, + 350 + ] + } + ], + "dependsOn": [ + "ai-field" + ] + } + ], + "cards": [ + { + "id": "fixed-step-rigid-body", + "node": "vehicle-dynamics", + "name": "Fixed-step rigid-body stepping", + "summary": "A 500 Hz outer step split into four 2 kHz substeps, fed from a capped frame accumulator", + "behavior": "In this browser build nothing is integrated at the display rate. Each frame's elapsed time is added to a pool and drained in fixed outer steps, and every outer step is split into four equal substeps, so tyre contact, suspension and drivetrain all solve at two kilohertz whether the tab is running at 30 frames or 144. The pool is capped at a tenth of a second, so a backgrounded tab or a paused debugger cannot bank a second of simulation and then dump it into one frame. Inside a substep the force and torque accumulators are cleared, each wheel's hardpoint is pushed into its current world position and its velocity taken from the body's linear plus rotational motion, gravity is applied to the sprung mass separately from the unsprung wheels, and the linear velocity is advanced before the body is translated and the angular velocity is rotated through the body-frame inertia tensor. The tyre torques a substep produces feed a drivetrain solve that sets the new wheel spin before the body is integrated, and a skid-block contact probes the floor at the front and rear for a stiff spring-damper with sliding friction, which is how a bottoming car is felt to scrape. The second car adapter uses the same half-millisecond outer step but caps catch-up at thirty steps per frame so a long stall cannot spiral.", + "constants": [ + { + "name": "fixedOuterStepSeconds", + "value": 0.002, + "unit": "seconds of simulated time per outer step", + "source": "src/lib/sim/f12026-car/VehiclePhysics.ts:599" + }, + { + "name": "substepsPerOuterStep", + "value": 4, + "unit": "substeps per outer step", + "source": "src/lib/sim/f12026-car/VehiclePhysics.ts:600" + }, + { + "name": "frameAccumulatorCapSeconds", + "value": 0.1, + "unit": "seconds of pooled frame time, per frame", + "source": "src/lib/sim/f12026-car/VehiclePhysics.ts:1160" + }, + { + "name": "gravityAcceleration", + "value": 9.81, + "unit": "metres per second squared, per substep", + "source": "src/lib/sim/f12026-car/VehiclePhysics.ts:115" + }, + { + "name": "contactSolverIterations", + "value": 8, + "unit": "iterations per substep", + "source": "src/lib/sim/f12026-car/VehiclePhysics.ts:120" + }, + { + "name": "plankSlidingFriction", + "value": 0.35, + "unit": "coefficient, per substep", + "source": "src/lib/sim/f12026-car/VehiclePhysics.ts:151" + }, + { + "name": "maxCatchUpOuterSteps", + "value": 30, + "unit": "outer steps per frame", + "source": "src/lib/f1-physics-2026.ts:1041" + } + ], + "sources": [ + { + "path": "src/lib/sim/f12026-car/VehiclePhysics.ts", + "line": 599 + }, + { + "path": "src/lib/sim/f12026-car/VehiclePhysics.ts", + "line": 1420 + }, + { + "path": "src/lib/sim/f12026-car/VehiclePhysics.ts", + "line": 1459 + }, + { + "path": "src/lib/sim/f12026-car/VehiclePhysics.ts", + "line": 1784 + }, + { + "path": "src/lib/f1-physics-2026.ts", + "line": 275 + } + ], + "capabilities": [ + "fixed timestep", + "substep integration", + "frame-rate independence", + "floor scraping" + ], + "tags": [ + "physics", + "timestep", + "integration", + "rigid-body" + ] + }, + { + "id": "tyre-slip-curve", + "node": "tyre-model", + "name": "Tyre slip curve and combined slip", + "summary": "A normalised slip vector fed through a fitted friction curve, with load sensitivity and a collapsing pneumatic trail", + "behavior": "Longitudinal and lateral slip are not used raw. Each is divided by its own peak value, the two are combined into one normalised slip magnitude that is capped, and that magnitude indexes a sine-of-arctangent curve whose slope at zero slip is fixed, which reaches its maximum at one and has fallen to the compound's falloff level by three times the peak. Coupling between the two directions is elliptical with a near-unity coefficient so a pure longitudinal or a pure lateral case stays exact while trail braking and corner-exit power blend smoothly. The available friction itself drops with load through an exponent taken from the compound data, so a heavily loaded tyre gives proportionally less grip, and it is scaled again by sliding speed, surface grip, temperature, pressure, wear and water. Below a few metres per second a carcass damping term fades in so the deflection spring does not ring at a standstill. The self-aligning moment is the lateral force times a pneumatic trail of roughly a sixth of the contact length, which collapses as the slip angle passes its peak and can even reverse slightly, which is what makes a sliding car go light and then pull into the slide through the wheel.", + "constants": [ + { + "name": "zeroSlipCurveSlope", + "value": 2, + "unit": "normalised force per normalised slip, per evaluation", + "source": "src/lib/sim/ac-tyre.ts:183" + }, + { + "name": "curveFalloffSlip", + "value": 3, + "unit": "multiples of peak slip, per evaluation", + "source": "src/lib/sim/ac-tyre.ts:185" + }, + { + "name": "storedSlipCap", + "value": 5, + "unit": "normalised slip, per evaluation", + "source": "src/lib/sim/ac-tyre.ts:179" + }, + { + "name": "lowSpeedBlendSpeed", + "value": 3, + "unit": "metres per second, per evaluation", + "source": "src/lib/sim/ac-tyre.ts:181" + }, + { + "name": "lowSpeedCarcassDamping", + "value": 3, + "unit": "newtons per metre per second per newton of load, per evaluation", + "source": "src/lib/sim/ac-tyre.ts:659" + }, + { + "name": "longitudinalLateralCoupling", + "value": 0.94, + "unit": "fraction, per evaluation", + "source": "src/lib/sim/ac-tyre.ts:646" + }, + { + "name": "pneumaticTrailDivisor", + "value": 6, + "unit": "divisor of the contact length, per evaluation", + "source": "src/lib/sim/ac-tyre.ts:672" + }, + { + "name": "defaultPeakSlipAngle", + "value": 7.5, + "unit": "degrees at the friction limit, per evaluation", + "source": "src/lib/sim/f12026-car/F12026CarConfig.ts:427" + } + ], + "sources": [ + { + "path": "src/lib/sim/ac-tyre.ts", + "line": 533 + }, + { + "path": "src/lib/sim/ac-tyre.ts", + "line": 646 + }, + { + "path": "src/lib/sim/ac-tyre.ts", + "line": 672 + }, + { + "path": "src/lib/sim/ac-tyre.ts", + "line": 343 + }, + { + "path": "src/lib/sim/f12026-car/F12026CarConfig.ts", + "line": 427 + } + ], + "capabilities": [ + "magic-formula slip curve", + "combined slip ellipse", + "load-sensitive friction", + "pneumatic trail" + ], + "tags": [ + "tyres", + "grip", + "handling", + "physics" + ] + }, + { + "id": "five-point-contact-patch", + "node": "tyre-model", + "name": "Five-point contact patch and road texture", + "summary": "The footprint is sampled at five points so kerbs, camber and road texture reach the tyre as a real surface normal and a pressure centroid", + "behavior": "Each tyre does not read a single ground height. Five probes, a centre, front, rear, inner and outer, are laid on the footprint whose half length grows with the square root of radius times deflection and whose half width is a little under half the section width. The effective contact height is a weighted envelope of those probes, mostly the centre with the corners sharing the rest, and the height differences across the footprint become a unit surface normal, a kerb-induced camber angle and a lateral shift of the pressure centroid that turns into an overturning moment. On top of the geometry sits a deterministic multi-octave spatial roughness field, so a lap is repeatable rather than random: a fraction of a millimetre on graded asphalt, several millimetres on grass, gravel and run-off, and on a kerb a fast sawtooth harmonic added to the fine noise. Because the whole thing is a pure function of world position it costs the same on every frame and survives a replay or a peer resync unchanged.", + "constants": [ + { + "name": "asphaltRoughnessM", + "value": 0.00055, + "unit": "metres of peak height, per sample", + "source": "src/lib/sim/ac-tyre.ts:228" + }, + { + "name": "looseSurfaceRoughnessM", + "value": 0.0038, + "unit": "metres of peak height, per sample", + "source": "src/lib/sim/ac-tyre.ts:225" + }, + { + "name": "kerbRumbleAmplitudeM", + "value": 0.0022, + "unit": "metres of peak height, per sample", + "source": "src/lib/sim/ac-tyre.ts:221" + }, + { + "name": "footprintHalfLengthMinM", + "value": 0.045, + "unit": "metres, per evaluation", + "source": "src/lib/sim/ac-tyre.ts:251" + }, + { + "name": "footprintHalfLengthMaxM", + "value": 0.14, + "unit": "metres, per evaluation", + "source": "src/lib/sim/ac-tyre.ts:251" + }, + { + "name": "footprintHalfWidthShare", + "value": 0.42, + "unit": "fraction of section width, per evaluation", + "source": "src/lib/sim/ac-tyre.ts:252" + }, + { + "name": "centreProbeWeight", + "value": 0.4, + "unit": "fraction of the effective height, per evaluation", + "source": "src/lib/sim/ac-tyre.ts:282" + }, + { + "name": "centroidShiftGain", + "value": 0.35, + "unit": "normalised shift per unit height gradient, per evaluation", + "source": "src/lib/sim/ac-tyre.ts:301" + } + ], + "sources": [ + { + "path": "src/lib/sim/ac-tyre.ts", + "line": 209 + }, + { + "path": "src/lib/sim/ac-tyre.ts", + "line": 237 + }, + { + "path": "src/lib/sim/ac-tyre.ts", + "line": 282 + }, + { + "path": "src/lib/sim/ac-tyre.ts", + "line": 301 + } + ], + "capabilities": [ + "five-point probe", + "surface normal from footprint", + "deterministic road texture", + "kerb camber" + ], + "tags": [ + "tyres", + "terrain", + "feedback", + "physics" + ] + }, + { + "id": "tyre-thermal-pressure", + "node": "tyre-model", + "name": "Nine-node tyre thermal network and inflation", + "summary": "Three tread bands across three layers plus the inflation gas, with grip read off each band's own temperature curve", + "behavior": "Heat into a tyre is tracked at nine nodes, an inner, middle and outer band each with a surface, bulk and carcass layer, plus the gas inside. Sliding power goes mostly into the surface nodes and a rolling hysteresis term into bulk and carcass, conduction runs surface to bulk to carcass and laterally between neighbouring bands, and each node loses heat to air, to the road under load and, for the gas, to the rim. Grip is not a single tyre temperature: each band's performance curve is evaluated at a weighted surface-and-bulk temperature, shifted by a viscoelastic frequency correction when the tyre is sliding, and the three results are weighted by each band's share of the contact pressure. That share moves with camber and with the lateral force, so an outer shoulder can be over its window while the inner one is still cold. Pressure comes from the ideal gas law between the measured cold temperature and a reference running temperature, and it feeds back three ways: a share of the radial stiffness scales with it, over-inflation crowns the tread and loads the centre band, and grip is lost with the square of the relative error from the optimum.", + "constants": [ + { + "name": "treadBands", + "value": 3, + "unit": "bands across the tread, per step", + "source": "src/lib/sim/ac-tyre.ts:41" + }, + { + "name": "coldReferenceC", + "value": 20, + "unit": "degrees Celsius at which cold pressures are quoted, per step", + "source": "src/lib/sim/ac-tyre.ts:187" + }, + { + "name": "referenceRunningC", + "value": 95, + "unit": "degrees Celsius, per step", + "source": "src/lib/sim/ac-tyre.ts:189" + }, + { + "name": "pressureStiffnessShare", + "value": 0.4, + "unit": "fraction of the radial rate, per step", + "source": "src/lib/sim/ac-tyre.ts:191" + }, + { + "name": "pressureGripLoss", + "value": 1.5, + "unit": "grip fraction per unit squared relative error, per step", + "source": "src/lib/sim/ac-tyre.ts:193" + }, + { + "name": "camberBandShift", + "value": 7, + "unit": "normalised band share per radian, per step", + "source": "src/lib/sim/ac-tyre.ts:195" + }, + { + "name": "lateralBandShift", + "value": 0.22, + "unit": "normalised band share per unit load transfer, per step", + "source": "src/lib/sim/ac-tyre.ts:197" + }, + { + "name": "crownBandGain", + "value": 2, + "unit": "centre-band share per unit relative over-pressure, per step", + "source": "src/lib/sim/ac-tyre.ts:199" + }, + { + "name": "surfaceTemperatureWeight", + "value": 0.6, + "unit": "fraction of the band temperature used for grip, per step", + "source": "src/lib/sim/ac-tyre.ts:524" + }, + { + "name": "surfaceNodeHeatCapacity", + "value": 2600, + "unit": "joules per kelvin across the tread, per step", + "source": "src/lib/sim/ac-tyre.ts:841" + }, + { + "name": "gasHeatCapacity", + "value": 80, + "unit": "joules per kelvin, per step", + "source": "src/lib/sim/ac-tyre.ts:845" + }, + { + "name": "surfaceBulkConductance", + "value": 95, + "unit": "watts per kelvin across the tread, per step", + "source": "src/lib/sim/ac-tyre.ts:846" + } + ], + "sources": [ + { + "path": "src/lib/sim/ac-tyre.ts", + "line": 518 + }, + { + "path": "src/lib/sim/ac-tyre.ts", + "line": 860 + }, + { + "path": "src/lib/sim/ac-tyre.ts", + "line": 464 + }, + { + "path": "src/lib/sim/ac-tyre.ts", + "line": 841 + } + ], + "capabilities": [ + "nine-node thermal model", + "ideal-gas pressure", + "band-weighted grip", + "camber-sensitive temperature" + ], + "tags": [ + "tyres", + "temperature", + "pressure", + "physics" + ] + }, + { + "id": "tyre-wear-flatspot", + "node": "tyre-model", + "name": "Tyre wear, flat spots and surface damage", + "summary": "Progressive wear loss, sixteen circumferential sectors that remember a lock-up, and graining and blistering windows", + "behavior": "Wear accumulates from sliding energy plus a load-and-speed rolling term and is scaled by the session's realism level and by how wet the road is. Grip does not fall linearly with it: a small linear loss applies from new, and a quadratic term only starts past the half-worn knee, so a scrubbed-in tyre holds its grip and then falls away quickly. Sixteen sectors around the circumference each keep their own wear and temperature, so a wheel that locks under braking concentrates its abrasion and heat on the single grounded sector and its two neighbours, and the difference between the most and least worn sectors becomes a flat-spot severity that drops the effective rolling radius and adds a rotation-order vibration through the wheel. Separately, a cold core being sheared by high sliding power raises a graining level that only scrubs clean while the core sits inside its working window, an overheating core raises a blistering level, and driving over grass, gravel or run-off, or over rubber marbles off the line, loads a dirt level that cuts grip until rolling and shear work clean it back toward whatever the local marble density allows.", + "constants": [ + { + "name": "wearGripLinearLoss", + "value": 0.06, + "unit": "grip fraction per unit wear, per step", + "source": "src/lib/sim/ac-tyre.ts:201" + }, + { + "name": "wearKnee", + "value": 0.5, + "unit": "unit wear, per step", + "source": "src/lib/sim/ac-tyre.ts:202" + }, + { + "name": "wearQuadraticLoss", + "value": 0.5, + "unit": "grip fraction per unit wear squared past the knee, per step", + "source": "src/lib/sim/ac-tyre.ts:203" + }, + { + "name": "circumferentialSectors", + "value": 16, + "unit": "sectors around the tread, per step", + "source": "src/lib/sim/ac-tyre.ts:43" + }, + { + "name": "lockupWearGain", + "value": 11, + "unit": "multiplier at full lock, per step", + "source": "src/lib/sim/ac-tyre.ts:740" + }, + { + "name": "flatSpotSeverityGain", + "value": 18, + "unit": "severity per unit wear spread, per step", + "source": "src/lib/sim/ac-tyre.ts:764" + }, + { + "name": "grainingCoreThresholdC", + "value": 78, + "unit": "degrees Celsius, per step", + "source": "src/lib/sim/ac-tyre.ts:783" + }, + { + "name": "grainingCleanUpperC", + "value": 82, + "unit": "degrees Celsius, per step", + "source": "src/lib/sim/ac-tyre.ts:787" + }, + { + "name": "blisterCoreThresholdC", + "value": 128, + "unit": "degrees Celsius, per step", + "source": "src/lib/sim/ac-tyre.ts:791" + }, + { + "name": "marblePickupShare", + "value": 0.6, + "unit": "fraction of local marble density, per step", + "source": "src/lib/sim/ac-tyre.ts:830" + } + ], + "sources": [ + { + "path": "src/lib/sim/ac-tyre.ts", + "line": 723 + }, + { + "path": "src/lib/sim/ac-tyre.ts", + "line": 773 + }, + { + "path": "src/lib/sim/ac-tyre.ts", + "line": 818 + }, + { + "path": "src/lib/sim/ac-tyre.ts", + "line": 338 + } + ], + "capabilities": [ + "progressive wear", + "sixteen-sector flat spotting", + "graining and blistering", + "dirt pickup" + ], + "tags": [ + "tyres", + "wear", + "damage", + "endurance" + ] + }, + { + "id": "double-wishbone-kinematics", + "node": "suspension", + "name": "Live double-wishbone geometry", + "summary": "Wheel travel is rotated through the real hardpoints, and camber gain, bump steer, trail and roll centres fall out of the geometry", + "behavior": "Suspension travel is not a lookup table of camber against bump. The lower wishbone is rotated about its chassis hinge axis until the upright's lower ball joint has risen by the travel, refined with a few Newton steps, and then the upper wishbone is rotated until the upright keeps its fixed length between the two ball joints. From the solved joints the code reads off the live kingpin axis, so camber gain in bump, kingpin inclination and caster come from the hardpoint positions rather than being authored, and the kingpin's ground intersection gives a scrub radius clamped to a physically plausible band and a caster trail that adds the kinematic change to the car's static figure. The tie rods give a mildly non-linear Ackermann split, and the mismatch between the tie-rod arc and the lower wishbone arc produces a small kinematic bump steer that is clamped to a few thousandths of a radian. On the axle, the front-view instant centres of the two corners put the roll centre where the contact-patch lines meet, and the side-view slopes give an anti-dive and anti-squat fraction, both within hard bounds; a rocker progressivity term stretches the motion ratio away from static ride height.", + "constants": [ + { + "name": "bumpTravelClamp", + "value": 0.08, + "unit": "metres, per solve", + "source": "src/lib/sim/suspension-kinematics.ts:292" + }, + { + "name": "upperWishboneIterations", + "value": 4, + "unit": "Newton iterations, per solve", + "source": "src/lib/sim/suspension-kinematics.ts:349" + }, + { + "name": "scrubRadiusMin", + "value": 0.008, + "unit": "metres, per solve", + "source": "src/lib/sim/suspension-kinematics.ts:381" + }, + { + "name": "scrubRadiusMax", + "value": 0.042, + "unit": "metres, per solve", + "source": "src/lib/sim/suspension-kinematics.ts:381" + }, + { + "name": "casterTrailChangeRange", + "value": 0.015, + "unit": "metres of kinematic change, per solve", + "source": "src/lib/sim/suspension-kinematics.ts:386" + }, + { + "name": "ackermannCoefficient", + "value": 0.14, + "unit": "dimensionless, per solve", + "source": "src/lib/sim/suspension-kinematics.ts:390" + }, + { + "name": "bumpSteerCap", + "value": 0.006, + "unit": "radians, per solve", + "source": "src/lib/sim/suspension-kinematics.ts:399" + }, + { + "name": "motionRatioMin", + "value": 0.85, + "unit": "ratio to static, per solve", + "source": "src/lib/sim/suspension-kinematics.ts:434" + }, + { + "name": "motionRatioMax", + "value": 1.28, + "unit": "ratio to static, per solve", + "source": "src/lib/sim/suspension-kinematics.ts:434" + }, + { + "name": "rollCentreHeightCapShare", + "value": 0.65, + "unit": "share of the loaded radius, per axle solve", + "source": "src/lib/sim/suspension-kinematics.ts:487" + }, + { + "name": "antiPitchMax", + "value": 0.35, + "unit": "fraction, per axle solve", + "source": "src/lib/sim/suspension-kinematics.ts:490" + } + ], + "sources": [ + { + "path": "src/lib/sim/suspension-kinematics.ts", + "line": 284 + }, + { + "path": "src/lib/sim/suspension-kinematics.ts", + "line": 461 + }, + { + "path": "src/lib/sim/suspension-kinematics.ts", + "line": 402 + }, + { + "path": "src/lib/sim/suspension-kinematics.ts", + "line": 487 + } + ], + "capabilities": [ + "three-dimensional wishbone solve", + "live camber and bump steer", + "roll-centre migration", + "anti-dive" + ], + "tags": [ + "suspension", + "geometry", + "handling", + "setup" + ] + }, + { + "id": "dirty-air-slipstream", + "node": "aero", + "name": "Downforce, drag and dirty air", + "summary": "Wing and floor loads from ride height and attitude, plus the wake a leading car leaves for the car behind", + "behavior": "Downforce is generated per wing and per floor element. Each one reads a lift and a drag coefficient from an angle-of-attack curve and a ride-height curve, multiplied by the dynamic pressure of the air the car actually meets, half the air density times the square of the forward airspeed, which is measured against the wind rather than the ground, so a headwind adds load. Bodywork lift at each axle is added on top, and the total is split between the axles by the element's position along the wheelbase before it is applied as a force at that point, so a rearward shift of the aerodynamic centre really does pitch the car. A following car sits in two different wakes: drag falls inside a tow, and downforce falls more sharply and more at the front than the rear, which shifts the centre of pressure rearward and makes the car understeer in traffic. The wake is built from every car between a couple of metres and about a hundred and thirty metres ahead, its half width spreading with distance, its strength growing with the leader's speed, its front-axle loss and rear-axle loss summed in quadrature so several cars saturate smoothly, and on a wet track it also carries a spray plume. Rolling the chassis lifts the inside floor edge and leaks the underfloor seal, which is modelled as a square-of-roll downforce loss with a floor.", + "constants": [ + { + "name": "dynamicPressureFactor", + "value": 0.5, + "unit": "dimensionless, per evaluation", + "source": "src/lib/sim/f12026-car/VehiclePhysics.ts:2176" + }, + { + "name": "towDragLossPerCar", + "value": 0.22, + "unit": "drag fraction per car ahead, per evaluation", + "source": "src/lib/sim/aero-wake.ts:114" + }, + { + "name": "dirtyAirFrontLossPerCar", + "value": 0.28, + "unit": "front downforce fraction per car ahead, per evaluation", + "source": "src/lib/sim/aero-wake.ts:115" + }, + { + "name": "dirtyAirRearLossPerCar", + "value": 0.16, + "unit": "rear downforce fraction per car ahead, per evaluation", + "source": "src/lib/sim/aero-wake.ts:116" + }, + { + "name": "dragLossCap", + "value": 0.26, + "unit": "fraction, per evaluation", + "source": "src/lib/sim/aero-wake.ts:121" + }, + { + "name": "frontDownforceLossCap", + "value": 0.32, + "unit": "fraction, per evaluation", + "source": "src/lib/sim/aero-wake.ts:122" + }, + { + "name": "rearDownforceLossCap", + "value": 0.2, + "unit": "fraction, per evaluation", + "source": "src/lib/sim/aero-wake.ts:123" + }, + { + "name": "wakeHalfWidthM", + "value": 1.05, + "unit": "metres at the rear wing, per evaluation", + "source": "src/lib/sim/aero-wake.ts:96" + }, + { + "name": "wakeSpreadPerM", + "value": 0.026, + "unit": "metres of half width per metre downstream, per evaluation", + "source": "src/lib/sim/aero-wake.ts:96" + }, + { + "name": "wakeNearLimitM", + "value": 130, + "unit": "metres downstream, per evaluation", + "source": "src/lib/sim/aero-wake.ts:91" + }, + { + "name": "frontDownforceWeight", + "value": 0.46, + "unit": "fraction of the mean downforce, per evaluation", + "source": "src/lib/sim/aero-wake.ts:130" + }, + { + "name": "rollFloorLeakCoefficient", + "value": 1.8, + "unit": "multiplier per radian squared of roll, per evaluation", + "source": "src/lib/sim/aero-wake.ts:151" + } + ], + "sources": [ + { + "path": "src/lib/sim/aero-wake.ts", + "line": 63 + }, + { + "path": "src/lib/sim/aero-wake.ts", + "line": 149 + }, + { + "path": "src/lib/sim/f12026-car/VehiclePhysics.ts", + "line": 2153 + }, + { + "path": "src/lib/sim/f12026-car/VehiclePhysics.ts", + "line": 2232 + } + ], + "capabilities": [ + "ride-height-sensitive wings", + "slipstream tow", + "dirty-air downforce loss", + "floor seal loss in roll" + ], + "tags": [ + "aero", + "downforce", + "drafting", + "physics" + ] + }, + { + "id": "drivetrain-ers-lsd", + "node": "drivetrain", + "name": "Engine, energy recovery and limited-slip differential", + "summary": "Torque from a power curve minus a coast curve, turbo spool, clutch take-up, deploy and harvest budgets, and a ramped differential solved by impulses", + "behavior": "Combustion torque is a power curve evaluated at engine speed and divided back into torque, minus a coast curve weighted by how far the throttle is closed, and the result is scaled by one of five engine maps that trade peak torque for a more aggressive pedal. A turbocharger chases a boost target that climbs with engine speed above the middle of the range, spooling in around a seventh of a second and decaying far faster, and while the boost trails its target a small share of the positive torque is withheld. The clutch is a smoothstep of engine speed between a bite point and a lock point, with a torque capacity proportional to engagement, so a launch slips and then locks. The electric machine either adds crank torque, limited by a speed-dependent deploy curve and by a per-lap deployment budget, or removes it to recharge, limited by a speed-dependent harvest curve and, under braking, by the rear axle's share of the pedal demand so harvesting can never brake the rears harder than the bias allows. The differential is solved as sequential impulses over a fixed iteration count: clutches cap the gearbox-to-axle and left-to-right transfers, a preload always acts, and power and coast ramp gains ride on the torque actually flowing through the diff, with a three-phase scheme picking entry, mid and exit coefficients from longitudinal acceleration, speed and a high-speed switch.", + "constants": [ + { + "name": "diffSolverIterations", + "value": 8, + "unit": "iterations per substep", + "source": "src/lib/sim/f12026-car/VehiclePhysics.ts:120" + }, + { + "name": "engineMapLowestTorqueScale", + "value": 0.87, + "unit": "fraction of full torque, per substep", + "source": "src/lib/sim/f12026-car/VehiclePhysics.ts:1943" + }, + { + "name": "turboSpoolSeconds", + "value": 0.14, + "unit": "seconds, per outer step", + "source": "src/lib/sim/f12026-car/VehiclePhysics.ts:1435" + }, + { + "name": "turboDecaySeconds", + "value": 0.05, + "unit": "seconds, per outer step", + "source": "src/lib/sim/f12026-car/VehiclePhysics.ts:1435" + }, + { + "name": "turboLagTorqueLoss", + "value": 0.1, + "unit": "fraction of positive torque, per substep", + "source": "src/lib/sim/f12026-car/VehiclePhysics.ts:141" + }, + { + "name": "diffLevelDivisor", + "value": 11, + "unit": "divisor mapping level twelve to one, per substep", + "source": "src/lib/sim/f12026-car/VehiclePhysics.ts:2067" + }, + { + "name": "diffHighSpeedSwitch", + "value": 250, + "unit": "kilometres per hour, per substep", + "source": "src/lib/sim/f12026-car/VehiclePhysics.ts:2061" + }, + { + "name": "defaultBrakeMigration", + "value": 0.015, + "unit": "front bias fraction, per substep", + "source": "src/lib/sim/f12026-car/VehiclePhysics.ts:2009" + }, + { + "name": "halfShaftTorqueShare", + "value": 0.5, + "unit": "fraction of the axle torque, per substep", + "source": "src/lib/sim/f12026-car/VehiclePhysics.ts:2054" + } + ], + "sources": [ + { + "path": "src/lib/sim/f12026-car/VehiclePhysics.ts", + "line": 1929 + }, + { + "path": "src/lib/sim/f12026-car/VehiclePhysics.ts", + "line": 1941 + }, + { + "path": "src/lib/sim/f12026-car/VehiclePhysics.ts", + "line": 1968 + }, + { + "path": "src/lib/sim/f12026-car/VehiclePhysics.ts", + "line": 2064 + }, + { + "path": "src/lib/sim/f12026-car/VehiclePhysics.ts", + "line": 2094 + } + ], + "capabilities": [ + "torque curve with coast", + "turbo lag", + "clutch take-up", + "deploy and harvest budget", + "ramped limited slip" + ], + "tags": [ + "drivetrain", + "engine", + "hybrid", + "differential" + ] + }, + { + "id": "brake-disc-thermal", + "node": "brakes-electronics", + "name": "Brake disc heat and pad bite", + "summary": "A two-node disc with convection, radiation and a double-sigmoid pad friction curve, plus bias migration under pedal", + "behavior": "Each corner's disc is two thermal nodes, a friction face and the internal core, joined by conduction. Friction power enters mostly at the face and the remainder conducts into the core; both lose heat by forced convection that rises with road speed, with the duct opening and with wet spray, and by radiation from a glowing face that follows the fourth power of absolute temperature. Pad friction is read from a double sigmoid of face temperature, so a carbon disc is soft when cold, holds full bite in a wide plateau and fades above roughly a thousand degrees, while a steel endurance disc comes up to temperature sooner and fades much lower; the result is floored so a totally faded brake still does something. Sustained operation above the glazing temperature accumulates a permanent pad glazing factor that shaves fifteen per cent off everything. Bias is not fixed: the driver's base front share migrates forward with pedal travel and comes back rearward as the pedal is released, clamped between hard limits, which is what makes a car stable under a big stop and rotate on the way into the corner.", + "constants": [ + { + "name": "carbonSurfaceHeatCapacity", + "value": 1150, + "unit": "joules per kelvin, per step", + "source": "src/lib/sim/brake-physics.ts:118" + }, + { + "name": "steelSurfaceHeatCapacity", + "value": 2100, + "unit": "joules per kelvin, per step", + "source": "src/lib/sim/brake-physics.ts:118" + }, + { + "name": "surfaceHeatShare", + "value": 0.62, + "unit": "fraction of friction power, per step", + "source": "src/lib/sim/brake-physics.ts:147" + }, + { + "name": "carbonColdBiteMidC", + "value": 90, + "unit": "degrees Celsius, per step", + "source": "src/lib/sim/brake-physics.ts:78" + }, + { + "name": "carbonFadeMidC", + "value": 1030, + "unit": "degrees Celsius, per step", + "source": "src/lib/sim/brake-physics.ts:80" + }, + { + "name": "steelFadeMidC", + "value": 790, + "unit": "degrees Celsius, per step", + "source": "src/lib/sim/brake-physics.ts:87" + }, + { + "name": "carbonDiscAreaM2", + "value": 0.135, + "unit": "square metres, per step", + "source": "src/lib/sim/brake-physics.ts:133" + }, + { + "name": "carbonEmissivity", + "value": 0.88, + "unit": "dimensionless, per step", + "source": "src/lib/sim/brake-physics.ts:132" + }, + { + "name": "carbonMaxDiscC", + "value": 1280, + "unit": "degrees Celsius, per step", + "source": "src/lib/sim/brake-physics.ts:150" + }, + { + "name": "carbonGlazeThresholdC", + "value": 980, + "unit": "degrees Celsius, per step", + "source": "src/lib/sim/brake-physics.ts:155" + }, + { + "name": "glazeRatePerSecond", + "value": 0.025, + "unit": "glazing fraction per second of overheat, per step", + "source": "src/lib/sim/brake-physics.ts:158" + }, + { + "name": "defaultMigrationRange", + "value": 0.06, + "unit": "front bias fraction, per braking event", + "source": "src/lib/sim/brake-physics.ts:175" + } + ], + "sources": [ + { + "path": "src/lib/sim/brake-physics.ts", + "line": 105 + }, + { + "path": "src/lib/sim/brake-physics.ts", + "line": 69 + }, + { + "path": "src/lib/sim/brake-physics.ts", + "line": 172 + }, + { + "path": "src/lib/sim/brake-physics.ts", + "line": 147 + } + ], + "capabilities": [ + "two-node disc thermal", + "temperature-dependent pad friction", + "pad glazing", + "brake migration" + ], + "tags": [ + "brakes", + "temperature", + "feel", + "physics" + ] + }, + { + "id": "traction-control-abs", + "node": "brakes-electronics", + "name": "Traction control and antilock brakes", + "summary": "Twelve-position slip targets with a two-stage torque cut, and four independent brake channels with deceleration anticipation", + "behavior": "Both aids are slip controllers with a position dial rather than a simple on-off. Traction control runs at a fixed rate on a weighted rear-axle slip ratio, mixing the worse wheel and the axle mean, and compares it with a target chosen from a twelve-entry table where position twelve is the most intrusive; the target is tightened as lateral acceleration builds and, on a wet track, as the rear slip angle grows, so the aid intervenes earlier in a corner than on a straight. The correction has two stages: below a small excess it retards timing for a partial cut, above it, cylinders are cut in sequence up to a much larger share, with a floor on the throttle share that moves with the second dial, and the cut is applied quickly and released at a quarter of that rate so the car does not snap back. Antilock brakes run four independent channels at their own higher rate, each combining the wheel's slip ratio with an anticipation term from wheel deceleration, comparing against a target from a twelve-entry table, and cutting that wheel's brake torque proportionally past it with a floor; the rear channels tighten their target when the car is trail braking with a big slip angle, which is what stops the rear axle from stepping out under a late brake release.", + "constants": [ + { + "name": "absSlipTargetMostIntrusive", + "value": 0.08, + "unit": "slip ratio, per update", + "source": "src/lib/sim/f12026-car/F12026CarSim.ts:97" + }, + { + "name": "absSlipTargetLeastIntrusive", + "value": 0.19, + "unit": "slip ratio, per update", + "source": "src/lib/sim/f12026-car/F12026CarSim.ts:97" + }, + { + "name": "absUpdateRateHz", + "value": 240, + "unit": "hertz, per update", + "source": "src/lib/sim/f12026-car/F12026CarSim.ts:99" + }, + { + "name": "tcSlipTargetMostIntrusive", + "value": 0.065, + "unit": "slip ratio, per update", + "source": "src/lib/sim/f12026-car/F12026CarSim.ts:100" + }, + { + "name": "tcSlipTargetLeastIntrusive", + "value": 0.15, + "unit": "slip ratio, per update", + "source": "src/lib/sim/f12026-car/F12026CarSim.ts:100" + }, + { + "name": "tcUpdateRateHz", + "value": 130, + "unit": "hertz, per update", + "source": "src/lib/sim/f12026-car/F12026CarSim.ts:102" + }, + { + "name": "tcMinimumSpeedKmh", + "value": 7.2, + "unit": "kilometres per hour, per update", + "source": "src/lib/sim/f12026-car/F12026CarSim.ts:103" + }, + { + "name": "tcStageBThreshold", + "value": 0.03, + "unit": "slip ratio excess, per update", + "source": "src/lib/sim/f12026-car/F12026CarSim.ts:1185" + }, + { + "name": "tcStageAShare", + "value": 0.25, + "unit": "fraction of torque cut, per update", + "source": "src/lib/sim/f12026-car/F12026CarSim.ts:1185" + }, + { + "name": "tcThrottleFloorBase", + "value": 0.18, + "unit": "throttle fraction, per update", + "source": "src/lib/sim/f12026-car/F12026CarSim.ts:1189" + }, + { + "name": "tcThrottleFloorPerLevel", + "value": 0.011, + "unit": "throttle fraction per level, per update", + "source": "src/lib/sim/f12026-car/F12026CarSim.ts:1189" + }, + { + "name": "rearAbsGuardFloor", + "value": 0.72, + "unit": "fraction of the target, per update", + "source": "src/lib/sim/f12026-car/F12026CarSim.ts:1213" + }, + { + "name": "absCutGain", + "value": 12.5, + "unit": "cut fraction per unit slip excess, per update", + "source": "src/lib/sim/f12026-car/F12026CarSim.ts:1215" + }, + { + "name": "absCutFloor", + "value": 0.08, + "unit": "brake torque fraction, per update", + "source": "src/lib/sim/f12026-car/F12026CarSim.ts:1215" + } + ], + "sources": [ + { + "path": "src/lib/sim/f12026-car/F12026CarSim.ts", + "line": 1162 + }, + { + "path": "src/lib/sim/f12026-car/F12026CarSim.ts", + "line": 1198 + }, + { + "path": "src/lib/sim/f12026-car/F12026CarSim.ts", + "line": 1204 + }, + { + "path": "src/lib/sim/f12026-car/F12026CarSim.ts", + "line": 1165 + } + ], + "capabilities": [ + "staged traction control", + "four-channel antilock", + "slip-angle guard under braking", + "adjustable intervention" + ], + "tags": [ + "assists", + "electronics", + "grip", + "braking" + ] + }, + { + "id": "surface-table", + "node": "surfaces", + "name": "Surface grip, ripple and vibration table", + "summary": "One data table per surface kind: friction dry and wet, rolling resistance, a real-wavelength ripple, vibration gain and track-limits membership", + "behavior": "What the tyres are rolling on is a small data table rather than code. Each kind of ground, road, kerb, sausage kerb, painted run-off, pit lane, grass and gravel, carries a dry friction multiplier, an extra wet multiplier relative to asphalt in the same water, a rolling resistance per newton of load, a sinusoidal ripple described by a real amplitude and a real wavelength, a vibration gain for force feedback and haptics, and a flag for whether it counts as track for the limits referee. Kerbs are the interesting row: slightly less grip than asphalt when dry but noticeably less when wet, a four millimetre ripple repeating every sixty-five centimetres, and full vibration gain. The visible kerb is a separate cross-section, a profile that rises from the road edge to a crown of just over three centimetres and back down over about a metre and a half, so the physics surface and the drawn surface agree. A circuit can override any field per kind in its own track data, and the ripple is added to the contact height analytically along the surface's own texture coordinate and faded in across its width, so the serrations are felt whatever the mesh density happens to be.", + "constants": [ + { + "name": "asphaltFriction", + "value": 1, + "unit": "grip multiplier in the dry, per contact", + "source": "src/lib/sim/track-surfaces.ts:35" + }, + { + "name": "kerbFriction", + "value": 0.96, + "unit": "grip multiplier in the dry, per contact", + "source": "src/lib/sim/track-surfaces.ts:37" + }, + { + "name": "kerbWetFriction", + "value": 0.72, + "unit": "extra multiplier at full wetness, per contact", + "source": "src/lib/sim/track-surfaces.ts:37" + }, + { + "name": "kerbRippleHeightM", + "value": 0.004, + "unit": "metres of peak ripple, per contact", + "source": "src/lib/sim/track-surfaces.ts:37" + }, + { + "name": "kerbRippleWavelengthM", + "value": 0.65, + "unit": "metres along the surface, per contact", + "source": "src/lib/sim/track-surfaces.ts:37" + }, + { + "name": "kerbCrownM", + "value": 0.032, + "unit": "metres above the road, per contact", + "source": "src/lib/sim/track-surfaces.ts:76" + }, + { + "name": "grassFriction", + "value": 0.6, + "unit": "grip multiplier in the dry, per contact", + "source": "src/lib/sim/track-surfaces.ts:43" + }, + { + "name": "grassRollingResistance", + "value": 0.04, + "unit": "drag per newton of load, per contact", + "source": "src/lib/sim/track-surfaces.ts:43" + }, + { + "name": "gravelFriction", + "value": 0.45, + "unit": "grip multiplier in the dry, per contact", + "source": "src/lib/sim/track-surfaces.ts:45" + }, + { + "name": "gravelRollingResistance", + "value": 0.16, + "unit": "drag per newton of load, per contact", + "source": "src/lib/sim/track-surfaces.ts:45" + } + ], + "sources": [ + { + "path": "src/lib/sim/track-surfaces.ts", + "line": 34 + }, + { + "path": "src/lib/sim/track-surfaces.ts", + "line": 53 + }, + { + "path": "src/lib/sim/track-surfaces.ts", + "line": 79 + }, + { + "path": "src/lib/sim/track-surfaces.ts", + "line": 93 + } + ], + "capabilities": [ + "per-surface friction", + "rolling resistance table", + "kerb ripple and crown", + "track-limits membership" + ], + "tags": [ + "terrain", + "grip", + "feedback", + "kerbs" + ] + }, + { + "id": "standing-water", + "node": "weather", + "name": "Rain film, drainage and aquaplaning", + "summary": "A water film that rain lays down and that drains, evaporates and gets squeezed off the racing line, plus the grip it takes away", + "behavior": "Water on the road is one global film depth with a per-sector memory of how much of it is still on the racing line. Rain adds depth at a rate that scales with the square of the intensity, so a heavy shower is far more than twice a light one. Depth leaves through run-off that grows faster than linearly with depth, which is why heavy rain settles at a couple of millimetres rather than accumulating without limit, and through evaporation that rises with track temperature, sun elevation, wind and dry air. Cars squeeze the line dry: a pass inside a few metres of the racing line clears a small share of the sector's water, weighted by a Gaussian across the line, so the dry line appears first where the field is running and the rain refills it within a minute of heavy weather. The tyre then reads that depth: a thin boundary film costs a little grip on any tyre, a slick loses a little more once the film is no longer merely damp, and above what the tread can clear at the current speed the tyre aquaplanes, with the tread's capacity quoted at a reference speed and falling as the tread wears. Standing water also lifts the front of the contact patch so the steering goes light before the tyre actually lets go, and adds a hydrodynamic drag force per tyre, so the side of the car that finds the deeper water is dragged back hardest.", + "constants": [ + { + "name": "dampFilmMm", + "value": 0.08, + "unit": "millimetres of film, per evaluation", + "source": "src/lib/sim/wet-tyres.ts:77" + }, + { + "name": "wetFilmMm", + "value": 0.5, + "unit": "millimetres of film, per evaluation", + "source": "src/lib/sim/wet-tyres.ts:78" + }, + { + "name": "standingWaterMm", + "value": 1.5, + "unit": "millimetres of film, per evaluation", + "source": "src/lib/sim/wet-tyres.ts:79" + }, + { + "name": "treadClearReferenceSpeed", + "value": 55.6, + "unit": "metres per second, per evaluation", + "source": "src/lib/sim/wet-tyres.ts:104" + }, + { + "name": "aquaplaneGripLossMax", + "value": 0.8, + "unit": "fraction of grip, per evaluation", + "source": "src/lib/sim/wet-tyres.ts:133" + }, + { + "name": "pneumaticTrailFloor", + "value": 0.25, + "unit": "multiplier, per evaluation", + "source": "src/lib/sim/wet-tyres.ts:159" + }, + { + "name": "hydrodynamicDragCoefficient", + "value": 0.16, + "unit": "dimensionless, per evaluation", + "source": "src/lib/sim/wet-tyres.ts:171" + }, + { + "name": "rainRateAtFullIntensity", + "value": 12, + "unit": "millimetres per hour, per weather step", + "source": "src/lib/sim/weather.ts:472" + }, + { + "name": "runoffCoefficient", + "value": 2, + "unit": "millimetres per hour per millimetre to the power of the exponent, per weather step", + "source": "src/lib/sim/weather.ts:474" + }, + { + "name": "runoffExponent", + "value": 1.67, + "unit": "dimensionless, per weather step", + "source": "src/lib/sim/weather.ts:475" + }, + { + "name": "lineClearedPerPass", + "value": 0.045, + "unit": "share of the sector film, per car pass", + "source": "src/lib/sim/weather.ts:477" + }, + { + "name": "lineHalfWidthM", + "value": 2.4, + "unit": "metres from the racing line, per weather step", + "source": "src/lib/sim/weather.ts:479" + }, + { + "name": "waterSectors", + "value": 64, + "unit": "sectors around the lap, per weather step", + "source": "src/lib/sim/weather.ts:481" + }, + { + "name": "trackTemperatureTimeConstant", + "value": 600, + "unit": "seconds, per weather step", + "source": "src/lib/sim/weather.ts:483" + } + ], + "sources": [ + { + "path": "src/lib/sim/wet-tyres.ts", + "line": 140 + }, + { + "path": "src/lib/sim/wet-tyres.ts", + "line": 126 + }, + { + "path": "src/lib/sim/wet-tyres.ts", + "line": 167 + }, + { + "path": "src/lib/sim/weather.ts", + "line": 664 + }, + { + "path": "src/lib/sim/weather.ts", + "line": 739 + } + ], + "capabilities": [ + "water film balance", + "line drying by traffic", + "aquaplaning with tread wear", + "steering lightening in water" + ], + "tags": [ + "weather", + "rain", + "grip", + "physics" + ] + }, + { + "id": "track-rubber-marbles", + "node": "track-state", + "name": "Rubbering-in, marbles and a green track", + "summary": "A per-sector rubber and marble state that changes the grip multiplier away from the racing line", + "behavior": "The circuit remembers what the field has done to it in sixty-four sectors. Driving within a couple of metres of the racing line lays warm rubber, damped by whatever dust or marbles are already there and rising asymptotically toward saturation; sliding hard in a corner throws rubber marbles outward onto the off-line surface, and driving off-line sweeps a small fraction of them away. The grip multiplier is then built from two pieces: a groove term that is zero at a natural baseline, rises with rubber and then bends over as the surface saturates, plus a marble penalty proportional to how much of the local surface is marbles and weighted by how far off the line the car is, all clamped into a narrow band around one. When the track is wet the rubbered-in line becomes slicker than the unrubbered asphalt, so the grip bonus fades as water fills the pores and then turns into a penalty. Rain washes the whole thing back toward a much less rubbered level on a long time constant, which is why a shower resets the weekend for everyone. Rubber laid on the line also grows with the sliding energy the tyres put in, not only with time on the line, so a busy first lap rubbers in faster than a quiet one, and a per-sector temperature rise from friction heating adds a little on top.", + "constants": [ + { + "name": "evolutionSectors", + "value": 64, + "unit": "sectors around the lap, per update", + "source": "src/lib/sim/track-evolution.ts:17" + }, + { + "name": "baselineRubber", + "value": 0.65, + "unit": "fraction of saturation, per update", + "source": "src/lib/sim/track-evolution.ts:18" + }, + { + "name": "baseTrackTemperatureC", + "value": 41, + "unit": "degrees Celsius at session start, per sample", + "source": "src/lib/sim/track-evolution.ts:57" + }, + { + "name": "washedRubberLevel", + "value": 0.35, + "unit": "fraction of saturation, per wash step", + "source": "src/lib/sim/track-evolution.ts:61" + }, + { + "name": "washTimeConstantS", + "value": 1200, + "unit": "seconds, per wash step", + "source": "src/lib/sim/track-evolution.ts:62" + }, + { + "name": "onLineHalfWidthM", + "value": 2.4, + "unit": "metres from the racing line, per update", + "source": "src/lib/sim/track-evolution.ts:146" + }, + { + "name": "surfaceTemperatureRiseCapC", + "value": 4.5, + "unit": "degrees Celsius, per update", + "source": "src/lib/sim/track-evolution.ts:168" + }, + { + "name": "grooveGripGain", + "value": 0.072, + "unit": "grip fraction per unit rubber, per sample", + "source": "src/lib/sim/track-evolution.ts:215" + }, + { + "name": "grooveGripQuadratic", + "value": 0.015, + "unit": "grip fraction per unit rubber squared, per sample", + "source": "src/lib/sim/track-evolution.ts:215" + }, + { + "name": "marbleGripPenalty", + "value": 0.075, + "unit": "grip fraction at full marble density, per sample", + "source": "src/lib/sim/track-evolution.ts:223" + }, + { + "name": "lineGaussianSigmaM", + "value": 2.1, + "unit": "metres, per sample", + "source": "src/lib/sim/track-evolution.ts:209" + }, + { + "name": "gripMultiplierFloor", + "value": 0.82, + "unit": "multiplier, per sample", + "source": "src/lib/sim/track-evolution.ts:226" + } + ], + "sources": [ + { + "path": "src/lib/sim/track-evolution.ts", + "line": 98 + }, + { + "path": "src/lib/sim/track-evolution.ts", + "line": 131 + }, + { + "path": "src/lib/sim/track-evolution.ts", + "line": 193 + }, + { + "path": "src/lib/sim/track-evolution.ts", + "line": 177 + } + ], + "capabilities": [ + "rubber build-up", + "marble shedding on the off-line", + "wet-groove inversion", + "rain resets the track" + ], + "tags": [ + "track", + "evolution", + "grip", + "session" + ] + }, + { + "id": "car-to-car-contact", + "node": "track-state", + "name": "Car-to-car rigid-body contact", + "summary": "Two oriented boxes tested with the separating axis theorem and resolved with impulses, lever arms and capped corrections", + "behavior": "Contact between cars is a proper rigid-body problem rather than a bump. Each car is an oriented rectangle in plan view, and the solver tries the four candidate separating axes, each car's forward and lateral directions, to find the axis of least overlap; a vertical band and a bounding-sphere test reject the pairs that cannot touch, so a car flying over a kerb does not collide with one on the road. The contact point is placed midway across both the normal and the tangential overlap, the relative velocity there includes the rotational lever arm, and a normal impulse with a low restitution plus a tangential friction impulse resolves it, with the point of application also producing a yaw change, so a hit on the rear quarter turns the car rather than sliding it sideways. The per-call position correction is capped, which keeps a deep interpenetration from teleporting a car out of the gravel. The opponent field uses its own variant with an explicit car mass and yaw inertia and hard per-frame caps on the position, velocity and yaw-rate changes, because a light touch lasting tens of milliseconds must not spin a car at racing speed.", + "constants": [ + { + "name": "collisionRestitution", + "value": 0.22, + "unit": "coefficient, per contact", + "source": "src/lib/sim/track-evolution.ts:344" + }, + { + "name": "collisionFriction", + "value": 0.28, + "unit": "coefficient, per contact", + "source": "src/lib/sim/track-evolution.ts:345" + }, + { + "name": "maxPositionCorrection", + "value": 0.25, + "unit": "metres per contact", + "source": "src/lib/sim/track-evolution.ts:321" + }, + { + "name": "verticalContactBand", + "value": 1.4, + "unit": "metres, per contact", + "source": "src/lib/sim/track-evolution.ts:348" + }, + { + "name": "contactMassKg", + "value": 778, + "unit": "kilograms, per contact", + "source": "src/lib/sim/ai-opponents.ts:232" + }, + { + "name": "contactYawInertia", + "value": 1120, + "unit": "kilogram metres squared, per contact", + "source": "src/lib/sim/ai-opponents.ts:233" + }, + { + "name": "contactMaxPositionChange", + "value": 0.25, + "unit": "metres per contact", + "source": "src/lib/sim/ai-opponents.ts:234" + }, + { + "name": "contactMaxVelocityChange", + "value": 6, + "unit": "metres per second per contact", + "source": "src/lib/sim/ai-opponents.ts:235" + }, + { + "name": "contactMaxYawChange", + "value": 0.25, + "unit": "radians per second per contact", + "source": "src/lib/sim/ai-opponents.ts:239" + } + ], + "sources": [ + { + "path": "src/lib/sim/track-evolution.ts", + "line": 341 + }, + { + "path": "src/lib/sim/track-evolution.ts", + "line": 348 + }, + { + "path": "src/lib/sim/ai-opponents.ts", + "line": 855 + }, + { + "path": "src/lib/sim/ai-opponents.ts", + "line": 310 + } + ], + "capabilities": [ + "oriented-box separating axes", + "impulse with lever arms", + "capped corrections", + "yaw from a quarter hit" + ], + "tags": [ + "collision", + "contact", + "multiplayer", + "physics" + ] + }, + { + "id": "ai-target-speed", + "node": "ai-field", + "name": "Opponent target speed and skill", + "summary": "Each AI car plans its own braking points from its own limit profile, then errors and skill scale the result", + "behavior": "An AI car does not chase a single authored speed trace. It first reads its own capability out of its limit-speed profile by looking at every place the profile drops and working out the deceleration that drop implies, binning that by speed and making the result non-decreasing with speed so downforce and drag are accounted for, with a floor of about eight tenths of a g at the lowest speed bin. It then builds a target profile: corners and braking zones take an apex scale, crests get a lift proportional to the negative vertical curvature and to a caution setting, and two passes backwards around the lap clamp every point to what can be reached by braking at a margin-reduced share of that capability. Pace and consistency come from a skill level anchored at four points, which sets a pace multiplier, a braking-margin addition, an apex multiplier, the size of the per-lap gaussian pace error and of the per-corner braking-point error, and the chance per lap of a genuine mistake into one corner. A per-section learner records where a car ran wide, locked up or lost the line and feeds those offsets back into its own braking margins and apex speeds on later laps, so a driver that keeps making the same error at one corner slowly stops making it.", + "constants": [ + { + "name": "decelarationEnvelopeFloor", + "value": 8, + "unit": "metres per second squared, per profile build", + "source": "src/lib/sim/ai-tuning.ts:436" + }, + { + "name": "gravity", + "value": 9.81, + "unit": "metres per second squared, per profile point", + "source": "src/lib/sim/ai-tuning.ts:418" + }, + { + "name": "brakingCurvatureThreshold", + "value": 0.004, + "unit": "inverse metres, per profile point", + "source": "src/lib/sim/ai-tuning.ts:473" + }, + { + "name": "targetSpeedFloor", + "value": 6, + "unit": "metres per second, per profile point", + "source": "src/lib/sim/ai-tuning.ts:478" + }, + { + "name": "backwardPassLaps", + "value": 2, + "unit": "passes around the lap, per profile build", + "source": "src/lib/sim/ai-tuning.ts:482" + }, + { + "name": "defaultConsistency", + "value": 0.85, + "unit": "fraction, per lap", + "source": "src/lib/sim/ai-tuning.ts:78" + }, + { + "name": "defaultOvertakeCaution", + "value": 0.3, + "unit": "fraction, per car", + "source": "src/lib/sim/ai-tuning.ts:75" + }, + { + "name": "defaultDefend", + "value": 0.4, + "unit": "fraction, per car", + "source": "src/lib/sim/ai-tuning.ts:76" + }, + { + "name": "paceRangeMin", + "value": 0.8, + "unit": "multiplier, per tuning file and per lap", + "source": "src/lib/sim/ai-tuning.ts:83" + }, + { + "name": "paceRangeMax", + "value": 1.05, + "unit": "multiplier, per tuning file and per lap", + "source": "src/lib/sim/ai-tuning.ts:83" + }, + { + "name": "defaultSkill", + "value": 70, + "unit": "zero to one hundred scale, per session", + "source": "src/lib/sim/ai-tuning.ts:210" + }, + { + "name": "proLapPaceSigma", + "value": 0.012, + "unit": "fraction of pace, per lap", + "source": "src/lib/sim/ai-tuning.ts:241" + }, + { + "name": "rookieMistakesPerLap", + "value": 0.3, + "unit": "mistakes per lap, per lap", + "source": "src/lib/sim/ai-tuning.ts:239" + } + ], + "sources": [ + { + "path": "src/lib/sim/ai-tuning.ts", + "line": 456 + }, + { + "path": "src/lib/sim/ai-tuning.ts", + "line": 424 + }, + { + "path": "src/lib/sim/ai-tuning.ts", + "line": 245 + }, + { + "path": "src/lib/sim/ai-tuning.ts", + "line": 482 + }, + { + "path": "src/lib/sim/ai-learning.ts", + "line": 107 + } + ], + "capabilities": [ + "per-car braking envelope", + "backward braking passes", + "skill anchors", + "section learning" + ], + "tags": [ + "ai", + "opponents", + "pace", + "difficulty" + ] + }, + { + "id": "ai-traffic-racecraft", + "node": "ai-field", + "name": "Following, passing and defending", + "summary": "Reaction gaps, side-by-side spacing, an inside cover when defending, and a lookahead that keeps a pass inside the track", + "behavior": "Traffic is planned before any AI car is stepped. Each car looks for three neighbours: one alongside within about a car length, one ahead inside ninety metres and inside a car's width of its own lateral position, and one closing from behind. Beside another car it simply holds a fixed lateral spacing away from it, and if that pushes it past the edge and it is not clear of the other car it lifts instead. To pass, it first checks whether it is already inside a reaction gap that grows with closing speed and with its own speed; if there is room on the wider side it aims that far across, and if there is not it sits in the tow and caps its speed at the car ahead minus a margin that shrinks as the gap opens. Before committing it looks a braking distance ahead for tightening curvature and limits how far it will run off its line accordingly, and gives the place up if a real corner arrives with a car still alongside. Defending covers the inside of the upcoming corner with a distance scaled by the car's own defend setting, started on the throttle and abandoned once in the corner so it can rejoin its line. Lateral movement is rate-limited, and the further it sits off the ideal line the more pace it gives away, which makes a car that has been forced wide genuinely slower. A car that has been stopped or off the road for a few seconds is placed back on its own line at a sane speed rather than left to flounder.", + "constants": [ + { + "name": "carLengthM", + "value": 5, + "unit": "metres, per frame", + "source": "src/lib/sim/ai-opponents.ts:73" + }, + { + "name": "carWidthM", + "value": 1.95, + "unit": "metres, per frame", + "source": "src/lib/sim/ai-opponents.ts:74" + }, + { + "name": "passSpacingM", + "value": 2.5, + "unit": "metres, per frame", + "source": "src/lib/sim/ai-opponents.ts:76" + }, + { + "name": "lineBiasRate", + "value": 2.2, + "unit": "metres per second, per frame", + "source": "src/lib/sim/ai-opponents.ts:78" + }, + { + "name": "startLightInterval", + "value": 1, + "unit": "seconds, per start", + "source": "src/lib/sim/ai-opponents.ts:79" + }, + { + "name": "baseCaution", + "value": 0.3, + "unit": "fraction, per frame", + "source": "src/lib/sim/ai-opponents.ts:82" + }, + { + "name": "mistakeApexScale", + "value": 1.06, + "unit": "multiplier, per lap", + "source": "src/lib/sim/ai-opponents.ts:84" + }, + { + "name": "reactionGapBaseM", + "value": 8, + "unit": "metres, per frame", + "source": "src/lib/sim/ai-opponents.ts:776" + }, + { + "name": "reactionGapClosingGain", + "value": 2.2, + "unit": "seconds, per frame", + "source": "src/lib/sim/ai-opponents.ts:776" + }, + { + "name": "followGapBaseM", + "value": 12, + "unit": "metres, per frame", + "source": "src/lib/sim/ai-opponents.ts:741" + }, + { + "name": "followGapCautionGain", + "value": 10, + "unit": "metres, per frame", + "source": "src/lib/sim/ai-opponents.ts:741" + }, + { + "name": "lookAheadBaseM", + "value": 30, + "unit": "metres, per frame", + "source": "src/lib/sim/ai-opponents.ts:795" + }, + { + "name": "lookAheadSpeedDivisor", + "value": 60, + "unit": "metres per squared metre per second, per frame", + "source": "src/lib/sim/ai-opponents.ts:795" + }, + { + "name": "tightLineBiasCap", + "value": 1.2, + "unit": "metres, per frame", + "source": "src/lib/sim/ai-opponents.ts:803" + }, + { + "name": "defendCoverM", + "value": 2.2, + "unit": "metres, per frame", + "source": "src/lib/sim/ai-opponents.ts:815" + }, + { + "name": "stuckRecoveryS", + "value": 4, + "unit": "seconds, per frame", + "source": "src/lib/sim/ai-opponents.ts:917" + } + ], + "sources": [ + { + "path": "src/lib/sim/ai-opponents.ts", + "line": 724 + }, + { + "path": "src/lib/sim/ai-opponents.ts", + "line": 650 + }, + { + "path": "src/lib/sim/ai-opponents.ts", + "line": 776 + }, + { + "path": "src/lib/sim/ai-opponents.ts", + "line": 803 + }, + { + "path": "src/lib/sim/ai-opponents.ts", + "line": 737 + } + ], + "capabilities": [ + "traffic detection", + "overtake with fallback to the tow", + "defensive inside cover", + "stuck car recovery" + ], + "tags": [ + "ai", + "racecraft", + "traffic", + "overtaking" + ] + }, + { + "id": "cockpit-shake-and-camera", + "node": "driver-input", + "name": "Cockpit shake and trackside cameras", + "summary": "Suspension velocity is high-passed into a small pitch, roll and heave shake, and an automatic director cuts between trackside dollies", + "behavior": "The cockpit view is not rigid. Each corner's suspension travel is differentiated to a velocity, high-passed to strip the steady attitude, and integrated with a slow leak so the estimate cannot drift, which leaves the fast part of what the wheels are doing. The four corners are combined into a front-to-rear difference for pitch, a left-to-right difference for roll and a mean for heave, scaled by the car's wheelbase and track, and those targets drive three spring-dampers that are integrated in two substeps and then clamped, with the velocity zeroed at the limit so they cannot wind up. Riding a kerb adds a one-off kick to roll, pitch and heave scaled by speed, plus a little noise for as long as the kerb is under a wheel, which is the part that makes the cockpit feel like it is being shaken rather than nudged. Separately, an automatic broadcast director places a fixed number of camera stations around the lap, on the outside of corners, each owning a short curved dolly track parallel to the road; the active station is the nearest by track distance with hysteresis so it does not flip back and forth, the look-at point is critically damped and leads the car, and the field of view is adjusted to keep the car a roughly constant size on screen and tightens a few degrees when the car is on a kerb.", + "constants": [ + { + "name": "shakeHighPassHz", + "value": 2, + "unit": "hertz, per frame", + "source": "src/lib/render/camera-rig.ts:13" + }, + { + "name": "shakeLeakSeconds", + "value": 0.5, + "unit": "seconds, per frame", + "source": "src/lib/render/camera-rig.ts:16" + }, + { + "name": "wheelbaseReferenceMm", + "value": 3400, + "unit": "millimetres, per frame", + "source": "src/lib/render/camera-rig.ts:18" + }, + { + "name": "trackWidthReferenceMm", + "value": 1460, + "unit": "millimetres, per frame", + "source": "src/lib/render/camera-rig.ts:19" + }, + { + "name": "pitchGain", + "value": 1.5, + "unit": "radians per millimetre of axle difference, per frame", + "source": "src/lib/render/camera-rig.ts:20" + }, + { + "name": "rollGain", + "value": 1.5, + "unit": "radians per millimetre of axle difference, per frame", + "source": "src/lib/render/camera-rig.ts:21" + }, + { + "name": "shakeFrequencyHz", + "value": 9, + "unit": "hertz, per frame", + "source": "src/lib/render/camera-rig.ts:24" + }, + { + "name": "shakeDampingRatio", + "value": 0.35, + "unit": "dimensionless, per frame", + "source": "src/lib/render/camera-rig.ts:25" + }, + { + "name": "pitchRollLimitDeg", + "value": 0.6, + "unit": "degrees, per frame", + "source": "src/lib/render/camera-rig.ts:27" + }, + { + "name": "heaveLimitM", + "value": 0.004, + "unit": "metres, per frame", + "source": "src/lib/render/camera-rig.ts:29" + }, + { + "name": "kerbRollKick", + "value": 0.25, + "unit": "radians per second, per kerb strike", + "source": "src/lib/render/camera-rig.ts:32" + }, + { + "name": "kerbNoise", + "value": 4, + "unit": "radians per second squared, per frame", + "source": "src/lib/render/camera-rig.ts:36" + }, + { + "name": "cameraStationSpacingM", + "value": 280, + "unit": "metres, per circuit", + "source": "src/lib/render/camera-rig.ts:203" + }, + { + "name": "cameraOffsetM", + "value": 32, + "unit": "metres from the centreline, per station", + "source": "src/lib/render/camera-rig.ts:211" + }, + { + "name": "cameraSwitchHysteresisM", + "value": 40, + "unit": "metres, per switch", + "source": "src/lib/render/camera-rig.ts:219" + }, + { + "name": "cameraFovMinDeg", + "value": 18, + "unit": "degrees, per frame", + "source": "src/lib/render/camera-rig.ts:222" + }, + { + "name": "cameraFovMaxDeg", + "value": 24, + "unit": "degrees, per frame", + "source": "src/lib/render/camera-rig.ts:223" + }, + { + "name": "kerbFovTightenDeg", + "value": 3, + "unit": "degrees, per frame", + "source": "src/lib/render/camera-rig.ts:225" + } + ], + "sources": [ + { + "path": "src/lib/render/camera-rig.ts", + "line": 70 + }, + { + "path": "src/lib/render/camera-rig.ts", + "line": 262 + }, + { + "path": "src/lib/render/camera-rig.ts", + "line": 106 + }, + { + "path": "src/lib/render/camera-rig.ts", + "line": 296 + } + ], + "capabilities": [ + "high-passed suspension shake", + "spring-damper cockpit motion", + "kerb strike kicks", + "automatic trackside director" + ], + "tags": [ + "camera", + "feel", + "feedback", + "broadcast" + ] + }, + { + "id": "wheel-force-feedback", + "node": "driver-input", + "name": "Steering weight and force feedback", + "summary": "The front axle's real steering torque, normalised, shaped, and delivered to a wheel through a native bridge or straight over browser HID", + "behavior": "What the driver feels at the rim is computed by the physics, not authored. For each front wheel the self-aligning moment is combined with the lateral force acting through the caster trail, a small share of the longitudinal force acting through the scrub radius and a share of the overturning moment, and the total is normalised by a reference torque and clamped, so a big moment at the contact patch becomes a definite but bounded weight at the rim. Flat-spot harmonics and road-texture noise ride on top. Because the browser cannot speak DirectInput, the shipped path streams one packet per frame over a local websocket to a small helper on the machine, which owns the wheel's constant-force effect, upsamples it, synthesises the kerb and antilock vibrations, rate-limits and fades to zero if the packets stop. A shaping stage applies the player's gain, adds a minimum force with a smoothstep blend near the centre so the rim does not notch, and gates that minimum force off at very low speed so a direct-drive wheel does not oscillate at rest. Past the car's own steering lock a soft-lock spring builds over a few degrees, standing in for the end stop a real rack has. An experimental path writes the standard HID force-feedback reports from the browser itself, discovering the report layout from the device rather than hard-coding vendors.", + "constants": [ + { + "name": "ffbReferenceTorqueNm", + "value": 600, + "unit": "newton metres, per substep", + "source": "src/lib/sim/f12026-car/VehiclePhysics.ts:139" + }, + { + "name": "defaultMechanicalTrailM", + "value": 0.012, + "unit": "metres, per substep", + "source": "src/lib/sim/f12026-car/VehiclePhysics.ts:145" + }, + { + "name": "steerComplianceTimeConstant", + "value": 0.015, + "unit": "seconds, per substep", + "source": "src/lib/sim/f12026-car/VehiclePhysics.ts:147" + }, + { + "name": "scrubRadiusFeedbackWeight", + "value": 0.15, + "unit": "dimensionless, per substep", + "source": "src/lib/sim/f12026-car/VehiclePhysics.ts:1753" + }, + { + "name": "overturningFeedbackWeight", + "value": 0.08, + "unit": "dimensionless, per substep", + "source": "src/lib/sim/f12026-car/VehiclePhysics.ts:1754" + }, + { + "name": "softLockRampDeg", + "value": 12, + "unit": "degrees past lock, per frame", + "source": "src/lib/input/wheel-ffb.ts:19" + }, + { + "name": "minimumForceEase", + "value": 0.02, + "unit": "fraction of full force, per frame", + "source": "src/lib/input/wheel-ffb.ts:21" + }, + { + "name": "bridgePort", + "value": 18500, + "unit": "port number, per connection", + "source": "src/lib/input/wheel-ffb.ts:15" + }, + { + "name": "hidWriteIntervalMs", + "value": 2, + "unit": "milliseconds, per report", + "source": "src/lib/input/webhid-ffb.ts:134" + }, + { + "name": "hidDirectionXFraction", + "value": 0.25, + "unit": "fraction of the direction range, per report", + "source": "src/lib/input/webhid-ffb.ts:138" + } + ], + "sources": [ + { + "path": "src/lib/sim/f12026-car/VehiclePhysics.ts", + "line": 1770 + }, + { + "path": "src/lib/input/wheel-ffb.ts", + "line": 39 + }, + { + "path": "src/lib/input/wheel-ffb.ts", + "line": 55 + }, + { + "path": "src/lib/sim/f12026-car/VehiclePhysics.ts", + "line": 1585 + } + ], + "capabilities": [ + "contact-patch steering torque", + "rim force shaping", + "soft lock", + "websocket bridge and browser HID" + ], + "tags": [ + "force feedback", + "input", + "feel", + "hardware" + ] + }, + { + "id": "gamepad-steering-assist", + "node": "driver-input", + "name": "Pad steering assist", + "summary": "A stick maps to a steering limit set by the front tyres' peak slip angle, with a self-aligning force that catches slides", + "behavior": "A thumbstick has a couple of centimetres of travel and no feedback, so mapping it straight to the rack makes a high-downforce car undrivable: a small twitch is several degrees of road-wheel angle at speed. Instead the stick, after a dead zone and a rate-limited smoothing, scales to a steering limit rather than to full lock, and that limit is set so the front tyres sit just under their peak slip angle relative to the front axle's actual direction of travel, which means full stick is maximum front grip at any speed. A self-steer term turns the wheels toward the rear axle's direction of travel with yaw damping, applied in full with the stick centred and fading out as the driver steers, which is what catches a slide. When the driver countersteers, the limit switches to the rear axle's travel angle plus a margin, so a full-lock correction is a sensible one rather than opposite lock, and a yaw-rate guard shrinks the limit when the car is already rotating faster than its grip can support. Because none of this acts at very low speed, the whole assist fades in between a couple of wheelbases of front-axle speed and out again for a car travelling sideways or backwards.", + "constants": [ + { + "name": "assistDeadzone", + "value": 0.05, + "unit": "fraction of stick travel, per frame", + "source": "src/lib/sim/gamepad-assist.ts:63" + }, + { + "name": "stickSmoothingRate", + "value": 0.5, + "unit": "rate multiplier, per frame", + "source": "src/lib/sim/gamepad-assist.ts:64" + }, + { + "name": "targetPeakSlipShare", + "value": 0.95, + "unit": "fraction of the peak slip angle, per frame", + "source": "src/lib/sim/gamepad-assist.ts:65" + }, + { + "name": "selfSteerResponse", + "value": 0.37, + "unit": "dimensionless, per frame", + "source": "src/lib/sim/gamepad-assist.ts:66" + }, + { + "name": "yawDampingStrength", + "value": 0.37, + "unit": "dimensionless, per frame", + "source": "src/lib/sim/gamepad-assist.ts:67" + }, + { + "name": "countersteerResponse", + "value": 0.2, + "unit": "fraction, per frame", + "source": "src/lib/sim/gamepad-assist.ts:68" + }, + { + "name": "maxDynamicLimitReduction", + "value": 5, + "unit": "zero to ten scale, per frame", + "source": "src/lib/sim/gamepad-assist.ts:69" + }, + { + "name": "countersteerDeadbandDeg", + "value": 4.5, + "unit": "degrees, per frame", + "source": "src/lib/sim/gamepad-assist.ts:199" + }, + { + "name": "yawGuardGain", + "value": 1.1, + "unit": "multiplier on the grip-limited yaw rate, per frame", + "source": "src/lib/sim/gamepad-assist.ts:208" + } + ], + "sources": [ + { + "path": "src/lib/sim/gamepad-assist.ts", + "line": 152 + }, + { + "path": "src/lib/sim/gamepad-assist.ts", + "line": 130 + }, + { + "path": "src/lib/sim/gamepad-assist.ts", + "line": 185 + }, + { + "path": "src/lib/sim/gamepad-assist.ts", + "line": 208 + } + ], + "capabilities": [ + "peak-slip steering limit", + "self-aligning force", + "countersteer limit", + "yaw-rate guard" + ], + "tags": [ + "input", + "assists", + "handling", + "gamepad" + ] + }, + { + "id": "race-timing", + "node": "race-control", + "name": "Lap clock, gaps and classification", + "summary": "Distance-bucket gap timing, a wrap-safe lap clock and a classification that handles penalties and lapped cars", + "behavior": "Race distance is the currency. Each car's progress is integrated from its track position with a wrapped difference, so crossing the line is a small step rather than a jump off a cliff, and a car starting behind the line simply has a negative distance until it crosses. Laps come from the whole part of that distance, and a lap is only recorded if it took more than a second, which filters out a reset or a teleport. Gaps are not measured car to car: the race time at which the first car reached each bucket of distance is remembered, and a car's gap is the current race time minus that remembered time, corrected for how far into the bucket the car has itself travelled, which is the interval since the leader was where this car is now; it stays correct even when the order changes. The classification sorts finishers by their time with penalties added and everyone else by distance, counts laps down from the leader's distance for cars still running, and closes some time after the winner in case a straggler never arrives. The start procedure runs on the same clock, one light at a time with a random hold after the last, so every client in a networked race shows the same lights and has the same instant of release.", + "constants": [ + { + "name": "gapBucketM", + "value": 25, + "unit": "metres, per frame", + "source": "src/lib/sim/race-session.ts:10" + }, + { + "name": "startLightIntervalMs", + "value": 1000, + "unit": "milliseconds, per start", + "source": "src/lib/sim/race-session.ts:11" + }, + { + "name": "classificationCloseS", + "value": 90, + "unit": "seconds after the winner, per race", + "source": "src/lib/sim/race-session.ts:13" + }, + { + "name": "minimumLapMs", + "value": 1000, + "unit": "milliseconds, per lap", + "source": "src/lib/sim/race-session.ts:267" + }, + { + "name": "splitTrendS", + "value": 4, + "unit": "seconds of history, per split", + "source": "src/lib/sim/race-session.ts:185" + } + ], + "sources": [ + { + "path": "src/lib/sim/race-session.ts", + "line": 259 + }, + { + "path": "src/lib/sim/race-session.ts", + "line": 198 + }, + { + "path": "src/lib/sim/race-session.ts", + "line": 45 + }, + { + "path": "src/lib/sim/race-session.ts", + "line": 235 + } + ], + "capabilities": [ + "wrap-safe lap counting", + "bucket gap timing", + "classification with penalties", + "synchronised start lights" + ], + "tags": [ + "race", + "timing", + "classification", + "multiplayer" + ] + }, + { + "id": "track-limits", + "node": "race-control", + "name": "Track limits and penalties", + "summary": "Excursions are judged after the car rejoins, with a give-back window, and repeat strikes become time penalties", + "behavior": "The referee does not invalidate a lap the instant a wheel crosses a line. An excursion is timed while it is happening and only judged once the car is back on the asphalt and has settled, so a kerb bounce is not a violation. A touch shorter than a fraction of a second is ignored outright. Cutting the inside of a corner is treated as a strike unless the time is given back, because there is no honest reason to be there; running wide is only a strike if the car actually gained time over its best lap, or, before there is a best lap to compare against, if the driver neither lifted nor braked. A strike can be cancelled by handing the advantage back inside a short window, either by losing the time gained or by lifting and coasting for long enough, with the owed lift proportional to the time spent off the track and clamped at both ends. Gaining a position off the track gets its own longer window to give it back. In practice a strike deletes the lap in practice or qualifying; in a race each strike is a warning and every third one is a time penalty that grows after the first.", + "constants": [ + { + "name": "minimumExcursionS", + "value": 0.12, + "unit": "seconds, per excursion", + "source": "src/lib/sim/track-limits.ts:18" + }, + { + "name": "rejoinDebounceS", + "value": 0.15, + "unit": "seconds, per excursion", + "source": "src/lib/sim/track-limits.ts:20" + }, + { + "name": "settleS", + "value": 1, + "unit": "seconds after the rejoin, per excursion", + "source": "src/lib/sim/track-limits.ts:22" + }, + { + "name": "gainThresholdS", + "value": 0.1, + "unit": "seconds over the reference lap, per excursion", + "source": "src/lib/sim/track-limits.ts:24" + }, + { + "name": "cutGainThresholdS", + "value": 0.03, + "unit": "seconds over the reference lap, per excursion", + "source": "src/lib/sim/track-limits.ts:26" + }, + { + "name": "giveBackWindowS", + "value": 6, + "unit": "seconds, per excursion", + "source": "src/lib/sim/track-limits.ts:28" + }, + { + "name": "illegalOvertakeWindowS", + "value": 10, + "unit": "seconds, per excursion", + "source": "src/lib/sim/track-limits.ts:30" + }, + { + "name": "noLiftThrottle", + "value": 0.6, + "unit": "throttle fraction, per judging step", + "source": "src/lib/sim/track-limits.ts:32" + }, + { + "name": "liftPerSecondOffTrack", + "value": 1, + "unit": "seconds of lift per second off, per excursion", + "source": "src/lib/sim/track-limits.ts:37" + }, + { + "name": "strikesPerPenalty", + "value": 3, + "unit": "strikes, per race", + "source": "src/lib/sim/track-limits.ts:45" + }, + { + "name": "firstPenaltyS", + "value": 3, + "unit": "seconds, per race", + "source": "src/lib/sim/track-limits.ts:46" + }, + { + "name": "repeatPenaltyS", + "value": 5, + "unit": "seconds, per race", + "source": "src/lib/sim/track-limits.ts:47" + } + ], + "sources": [ + { + "path": "src/lib/sim/track-limits.ts", + "line": 104 + }, + { + "path": "src/lib/sim/track-limits.ts", + "line": 153 + }, + { + "path": "src/lib/sim/track-limits.ts", + "line": 45 + }, + { + "path": "src/lib/sim/track-limits.ts", + "line": 18 + } + ], + "capabilities": [ + "judged-on-rejoin excursions", + "time-gained detection", + "give-back window", + "escalating penalties" + ], + "tags": [ + "race control", + "rules", + "penalties", + "fairness" + ] + } + ] +} diff --git a/data/games/mario-kart-double-dash.json b/data/games/mario-kart-double-dash.json new file mode 100644 index 0000000..a0806ea --- /dev/null +++ b/data/games/mario-kart-double-dash.json @@ -0,0 +1,2790 @@ +{ + "version": 1, + "id": "mario-kart-double-dash", + "title": "Mario Kart: Double Dash!!", + "repo": "https://github.com/doldecomp/mkdd", + "sha": "b56cc7677fa23058eff8f38784479c3b4b854060", + "nodes": [ + { + "id": "kart-body", + "name": "Kart rigid body", + "purpose": "Per-update rigid body integration for every kart: box mass and inertia, gravity selection, accumulated force and torque, angular velocity solve and the pose write-back that places the model in the world", + "paths": [ + "src/Yamamoto/kartBody.cpp" + ], + "functions": [ + { + "name": "KartBody::MakeMassBody", + "path": "src/Yamamoto/kartBody.cpp", + "lines": [ + 267, + 277 + ] + }, + { + "name": "KartBody::MakeRigidVertexBody", + "path": "src/Yamamoto/kartBody.cpp", + "lines": [ + 279, + 281 + ] + }, + { + "name": "KartBody::DoCalcMassMatrix", + "path": "src/Yamamoto/kartBody.cpp", + "lines": [ + 578, + 594 + ] + }, + { + "name": "KartBody::DoGForce", + "path": "src/Yamamoto/kartBody.cpp", + "lines": [ + 429, + 464 + ] + }, + { + "name": "KartBody::DoCalcWgVel", + "path": "src/Yamamoto/kartBody.cpp", + "lines": [ + 472, + 525 + ] + }, + { + "name": "KartBody::DoPose", + "path": "src/Yamamoto/kartBody.cpp", + "lines": [ + 527, + 576 + ] + }, + { + "name": "KartBody::CalcMovement", + "path": "src/Yamamoto/kartBody.cpp", + "lines": [ + 466, + 470 + ] + }, + { + "name": "KartBody::DoForce", + "path": "src/Yamamoto/kartBody.cpp", + "lines": [ + 387, + 395 + ] + }, + { + "name": "KartBody::SetUpRigidBodyPose", + "path": "src/Yamamoto/kartBody.cpp", + "lines": [ + 283, + 317 + ] + } + ], + "dependsOn": [] + }, + { + "id": "kart-contact", + "parent": "kart-body", + "name": "Kart contact resolution", + "purpose": "What the body does when it touches something: ground impulse with tangential friction, wall and vertex reflection, object reflection and kart-versus-kart mass-weighted bouncing", + "paths": [ + "src/Yamamoto/kartBody.cpp" + ], + "functions": [ + { + "name": "KartBody::GroundReflection", + "path": "src/Yamamoto/kartBody.cpp", + "lines": [ + 638, + 693 + ] + }, + { + "name": "KartBody::GroundVertexReflection", + "path": "src/Yamamoto/kartBody.cpp", + "lines": [ + 1216, + 1235 + ] + }, + { + "name": "KartBody::WallFriction", + "path": "src/Yamamoto/kartBody.cpp", + "lines": [ + 596, + 636 + ] + }, + { + "name": "KartBody::WallReflection", + "path": "src/Yamamoto/kartBody.cpp", + "lines": [ + 1178, + 1210 + ] + }, + { + "name": "KartBody::WallVertexReflection", + "path": "src/Yamamoto/kartBody.cpp", + "lines": [ + 1237, + 1277 + ] + }, + { + "name": "KartBody::CheckVertexReflection", + "path": "src/Yamamoto/kartBody.cpp", + "lines": [ + 1279, + 1348 + ] + }, + { + "name": "KartBody::RigidReflection", + "path": "src/Yamamoto/kartBody.cpp", + "lines": [ + 837, + 943 + ] + }, + { + "name": "KartBody::RigidReflectionPower", + "path": "src/Yamamoto/kartBody.cpp", + "lines": [ + 743, + 835 + ] + }, + { + "name": "KartBody::ObjectReflection", + "path": "src/Yamamoto/kartBody.cpp", + "lines": [ + 945, + 976 + ] + }, + { + "name": "KartBody::CheckBodyHit", + "path": "src/Yamamoto/kartBody.cpp", + "lines": [ + 1350, + 1436 + ] + } + ], + "dependsOn": [ + "kart-body" + ] + }, + { + "id": "kart-math", + "name": "Kart math utilities", + "purpose": "Small linear algebra and smoothing set the kart code leans on: a Gaussian elimination solve for the contact mass matrix, a fast reciprocal square root, spherical-to-cartesian conversion through angle lookups, and the easing helpers used for every ramped value", + "paths": [ + "src/Yamamoto/kartvec.cpp" + ], + "functions": [ + { + "name": "KartMath::Gauss", + "path": "src/Yamamoto/kartvec.cpp", + "lines": [ + 261, + 271 + ] + }, + { + "name": "KartMath::lu", + "path": "src/Yamamoto/kartvec.cpp", + "lines": [ + 155, + 229 + ] + }, + { + "name": "KartMath::solve", + "path": "src/Yamamoto/kartvec.cpp", + "lines": [ + 231, + 259 + ] + }, + { + "name": "KartMath::SpeedySqrtf", + "path": "src/Yamamoto/kartvec.cpp", + "lines": [ + 108, + 117 + ] + }, + { + "name": "KartMath::ChaseFnumber", + "path": "src/Yamamoto/kartvec.cpp", + "lines": [ + 326, + 334 + ] + }, + { + "name": "KartMath::ChaseFcnvge4", + "path": "src/Yamamoto/kartvec.cpp", + "lines": [ + 347, + 358 + ] + }, + { + "name": "KartMath::PolarToCross", + "path": "src/Yamamoto/kartvec.cpp", + "lines": [ + 132, + 153 + ] + }, + { + "name": "KartMath::CrossToPolar", + "path": "src/Yamamoto/kartvec.cpp", + "lines": [ + 119, + 130 + ] + } + ], + "dependsOn": [] + }, + { + "id": "kart-control", + "name": "Kart control manager", + "purpose": "The static manager that owns every kart body and answers the queries the rest of the game needs: per-surface top speed, down-slope bonuses, water depth and the player-versus-partner controller split", + "paths": [ + "src/Yamamoto/kartCtrl.cpp", + "src/Yamamoto/kartCtrlInfo.cpp" + ], + "functions": [ + { + "name": "KartCtrl::Init", + "path": "src/Yamamoto/kartCtrl.cpp", + "lines": [ + 15, + 55 + ] + }, + { + "name": "KartCtrl::DoBodyHit", + "path": "src/Yamamoto/kartCtrl.cpp", + "lines": [ + 57, + 82 + ] + }, + { + "name": "KartCtrl::GetMaxSpeed", + "path": "src/Yamamoto/kartCtrlInfo.cpp", + "lines": [ + 432, + 482 + ] + }, + { + "name": "KartCtrl::GetDownSlopeAcc", + "path": "src/Yamamoto/kartCtrlInfo.cpp", + "lines": [ + 484, + 501 + ] + }, + { + "name": "KartCtrl::GetDownSlopeSpeed", + "path": "src/Yamamoto/kartCtrlInfo.cpp", + "lines": [ + 503, + 518 + ] + }, + { + "name": "KartCtrl::GetWaterHeight", + "path": "src/Yamamoto/kartCtrlInfo.cpp", + "lines": [ + 528, + 539 + ] + }, + { + "name": "KartCtrl::GetDriftCnt", + "path": "src/Yamamoto/kartCtrlInfo.cpp", + "lines": [ + 676, + 683 + ] + } + ], + "dependsOn": [ + "kart-body", + "kart-math" + ] + }, + { + "id": "kart-crash", + "parent": "kart-body", + "name": "Crash and spin states", + "purpose": "Timed kart states entered when an item or hazard lands: full spin, half spin, bomb launch, tornado throw, burn and freeze, each with the angular ramp that carries the body through the state and back to driving", + "paths": [ + "src/Yamamoto/KartCrash.cpp" + ], + "functions": [ + { + "name": "KartCrash::MakeSpin", + "path": "src/Yamamoto/KartCrash.cpp", + "lines": [ + 430, + 468 + ] + }, + { + "name": "KartCrash::DoSpinCrashCrl", + "path": "src/Yamamoto/KartCrash.cpp", + "lines": [ + 470, + 501 + ] + }, + { + "name": "KartCrash::MakeHalfSpin", + "path": "src/Yamamoto/KartCrash.cpp", + "lines": [ + 503, + 540 + ] + }, + { + "name": "KartCrash::DoHalfSpinCrashCrl", + "path": "src/Yamamoto/KartCrash.cpp", + "lines": [ + 542, + 574 + ] + }, + { + "name": "KartCrash::MakeTornado", + "path": "src/Yamamoto/KartCrash.cpp", + "lines": [ + 309, + 349 + ] + }, + { + "name": "KartCrash::DotornadeCrashCrl", + "path": "src/Yamamoto/KartCrash.cpp", + "lines": [ + 369, + 428 + ] + }, + { + "name": "KartCrash::MakeBombCrash", + "path": "src/Yamamoto/KartCrash.cpp", + "lines": [ + 233, + 260 + ] + }, + { + "name": "KartCrash::MakeFreezeCrash", + "path": "src/Yamamoto/KartCrash.cpp", + "lines": [ + 608, + 632 + ] + } + ], + "dependsOn": [ + "kart-body", + "kart-control" + ] + }, + { + "id": "kart-status", + "parent": "kart-body", + "name": "Star, thunder and damage states", + "purpose": "Long-running kart buffs and debuffs: star invulnerability and its countdown, the thunder shrink-and-recover cycle, and the four damage reaction timers that lock a driver out of steering", + "paths": [ + "src/Yamamoto/KartStar.cpp", + "src/Yamamoto/KartThunder.cpp", + "src/Yamamoto/KartDamage.cpp" + ], + "functions": [ + { + "name": "KartStar::MakeStar", + "path": "src/Yamamoto/KartStar.cpp", + "lines": [ + 15, + 29 + ] + }, + { + "name": "KartStar::DoStarCrl", + "path": "src/Yamamoto/KartStar.cpp", + "lines": [ + 40, + 47 + ] + }, + { + "name": "KartThunder::MakeThunder", + "path": "src/Yamamoto/KartThunder.cpp", + "lines": [ + 48, + 74 + ] + }, + { + "name": "KartThunder::DoThunderCrl", + "path": "src/Yamamoto/KartThunder.cpp", + "lines": [ + 76, + 113 + ] + }, + { + "name": "KartThunder::DoShake", + "path": "src/Yamamoto/KartThunder.cpp", + "lines": [ + 27, + 34 + ] + }, + { + "name": "KartDamage::MakeDamage", + "path": "src/Yamamoto/KartDamage.cpp", + "lines": [ + 247, + 253 + ] + }, + { + "name": "KartDamage::MakeHalfDamage", + "path": "src/Yamamoto/KartDamage.cpp", + "lines": [ + 210, + 245 + ] + }, + { + "name": "KartDamage::MakeBigDamage", + "path": "src/Yamamoto/KartDamage.cpp", + "lines": [ + 255, + 263 + ] + }, + { + "name": "KartDamage::MakeBurnDamage", + "path": "src/Yamamoto/KartDamage.cpp", + "lines": [ + 265, + 271 + ] + }, + { + "name": "KartDamage::SetDamageAnime", + "path": "src/Yamamoto/KartDamage.cpp", + "lines": [ + 124, + 157 + ] + } + ], + "dependsOn": [ + "kart-body", + "kart-control" + ] + }, + { + "id": "kart-camera", + "name": "Kart chase camera", + "purpose": "Chooses which camera mode a view uses each update and places the chase, fixed, search and path cameras relative to the kart and to authored camera data", + "paths": [ + "src/Yamamoto/kartCamera.cpp", + "src/Yamamoto/KartPerCam.cpp" + ], + "functions": [ + { + "name": "KartCam::DoCamera", + "path": "src/Yamamoto/kartCamera.cpp", + "lines": [ + 575, + 597 + ] + }, + { + "name": "KartCam::DoRaceCtrl", + "path": "src/Yamamoto/kartCamera.cpp", + "lines": [ + 455, + 524 + ] + }, + { + "name": "KartCam::DoCameraMode", + "path": "src/Yamamoto/kartCamera.cpp", + "lines": [ + 526, + 573 + ] + }, + { + "name": "KartPerCam::InitChaseCam", + "path": "src/Yamamoto/KartPerCam.cpp", + "lines": [ + 109, + 127 + ] + }, + { + "name": "KartPerCam::InitFixCam", + "path": "src/Yamamoto/KartPerCam.cpp", + "lines": [ + 45, + 61 + ] + }, + { + "name": "KartPerCam::ChaseFovy", + "path": "src/Yamamoto/KartPerCam.cpp", + "lines": [ + 22, + 43 + ] + } + ], + "dependsOn": [ + "kart-control" + ] + }, + { + "id": "kart-roster", + "name": "Kart and driver attribute tables", + "purpose": "Per-driver and per-vehicle attribute records: driver weight class, default partner and default vehicle, plus per-vehicle weight class and wheel count, all reached through lookup switches", + "paths": [ + "src/Kaneshige/KartInfo.cpp" + ], + "functions": [ + { + "name": "KartInfo::getCharDB", + "path": "src/Kaneshige/KartInfo.cpp", + "lines": [ + 363, + 429 + ] + }, + { + "name": "KartInfo::getKartDB", + "path": "src/Kaneshige/KartInfo.cpp", + "lines": [ + 431, + 500 + ] + }, + { + "name": "KartInfo::getKartWeight", + "path": "src/Kaneshige/KartInfo.cpp", + "lines": [ + 502, + 504 + ] + }, + { + "name": "KartInfo::getDriverWeight", + "path": "src/Kaneshige/KartInfo.cpp", + "lines": [ + 514, + 516 + ] + } + ], + "dependsOn": [] + }, + { + "id": "race-flow", + "name": "Race manager", + "purpose": "Owns the race frame: stage ordering for the physics and object updates, the ranking pass for every race and battle mode, and the start-grid placement handed to the karts", + "paths": [ + "src/Kaneshige/RaceMgr.cpp" + ], + "functions": [ + { + "name": "RaceMgr::frameWork", + "path": "src/Kaneshige/RaceMgr.cpp", + "lines": [ + 1164, + 1193 + ] + }, + { + "name": "RaceMgr::updateRace", + "path": "src/Kaneshige/RaceMgr.cpp", + "lines": [ + 1195, + 1278 + ] + }, + { + "name": "RaceMgr::calcRace", + "path": "src/Kaneshige/RaceMgr.cpp", + "lines": [ + 1122, + 1162 + ] + }, + { + "name": "RaceMgr::checkRank", + "path": "src/Kaneshige/RaceMgr.cpp", + "lines": [ + 792, + 818 + ] + }, + { + "name": "RaceMgr::checkRankForRace", + "path": "src/Kaneshige/RaceMgr.cpp", + "lines": [ + 1043, + 1107 + ] + }, + { + "name": "RaceMgr::isAbleStart", + "path": "src/Kaneshige/RaceMgr.cpp", + "lines": [ + 1342, + 1351 + ] + } + ], + "dependsOn": [ + "lap-tracking", + "kart-control" + ] + }, + { + "id": "lap-tracking", + "name": "Lap and sector tracking", + "purpose": "Converts a kart's world position into fractional progress between checkpoint planes, accumulates that into race progress and decides laps, goals, shortcuts and reverse driving", + "paths": [ + "src/Kaneshige/KartChecker.cpp", + "src/Kaneshige/Course/Course.cpp" + ], + "functions": [ + { + "name": "KartChecker::checkKart", + "path": "src/Kaneshige/KartChecker.cpp", + "lines": [ + 367, + 385 + ] + }, + { + "name": "KartChecker::checkKartLap", + "path": "src/Kaneshige/KartChecker.cpp", + "lines": [ + 387, + 473 + ] + }, + { + "name": "KartChecker::searchCurrentSector", + "path": "src/Kaneshige/KartChecker.cpp", + "lines": [ + 260, + 365 + ] + }, + { + "name": "KartChecker::setLapTime", + "path": "src/Kaneshige/KartChecker.cpp", + "lines": [ + 603, + 658 + ] + }, + { + "name": "KartChecker::isReverse", + "path": "src/Kaneshige/KartChecker.cpp", + "lines": [ + 758, + 775 + ] + }, + { + "name": "Course::Sector::calcUnitDist", + "path": "src/Kaneshige/Course/Course.cpp", + "lines": [ + 1779, + 1799 + ] + }, + { + "name": "Course::Sector::isLRInside", + "path": "src/Kaneshige/Course/Course.cpp", + "lines": [ + 1801, + 1823 + ] + } + ], + "dependsOn": [ + "course-terrain" + ] + }, + { + "id": "course-terrain", + "name": "Course ground and collision grid", + "purpose": "Queries the static course collision mesh: candidate selection through a uniform X-Z grid, sphere-versus-polygon depth tests, wall normal and depth rounding, plus the surface attribute and material codes the kart physics reads", + "paths": [ + "src/Kaneshige/Course/CrsGround.cpp" + ], + "functions": [ + { + "name": "CrsGround::search", + "path": "src/Kaneshige/Course/CrsGround.cpp", + "lines": [ + 197, + 388 + ] + }, + { + "name": "CrsGround::checkGridBySphere", + "path": "src/Kaneshige/Course/CrsGround.cpp", + "lines": [ + 399, + 504 + ] + }, + { + "name": "CrsGround::roundDepth", + "path": "src/Kaneshige/Course/CrsGround.cpp", + "lines": [ + 390, + 397 + ] + }, + { + "name": "CrsGround::searchGridIndex", + "path": "src/Kaneshige/Course/CrsGround.cpp", + "lines": [ + 671, + 692 + ] + }, + { + "name": "CrsGround::isInsideGrid", + "path": "src/Kaneshige/Course/CrsGround.cpp", + "lines": [ + 650, + 669 + ] + } + ], + "dependsOn": [] + }, + { + "id": "item-system", + "name": "Item economy", + "purpose": "Tracks which of the two tandem drivers holds or stocks which item, moves items between them and between karts on contact, caps how many items may exist in the world and recycles item boxes", + "paths": [ + "src/Sato/ItemObjMgr.cpp", + "src/Sato/GeoItemBox.cpp" + ], + "functions": [ + { + "name": "ItemObjMgr::initItemMaxNums", + "path": "src/Sato/ItemObjMgr.cpp", + "lines": [ + 159, + 174 + ] + }, + { + "name": "ItemObjMgr::createShuffleMgr", + "path": "src/Sato/ItemObjMgr.cpp", + "lines": [ + 176, + 193 + ] + }, + { + "name": "ItemObjMgr::setThunderDisableUseCounter", + "path": "src/Sato/ItemObjMgr.cpp", + "lines": [ + 1195, + 1203 + ] + }, + { + "name": "ItemObjMgr::getNowTandemDriverNum", + "path": "src/Sato/ItemObjMgr.cpp", + "lines": [ + 1205, + 1220 + ] + }, + { + "name": "ItemObjMgr::robRivalOfItem", + "path": "src/Sato/ItemObjMgr.cpp", + "lines": [ + 505, + 546 + ] + }, + { + "name": "ItemObjMgr::transferPartnerOfItem", + "path": "src/Sato/ItemObjMgr.cpp", + "lines": [ + 548, + 590 + ] + }, + { + "name": "ItemObjMgr::stockItemToKart", + "path": "src/Sato/ItemObjMgr.cpp", + "lines": [ + 592, + 603 + ] + }, + { + "name": "GeoItemBox::doInactive", + "path": "src/Sato/GeoItemBox.cpp", + "lines": [ + 302, + 347 + ] + }, + { + "name": "GeoItemBox::setItemSlot", + "path": "src/Sato/GeoItemBox.cpp", + "lines": [ + 244, + 259 + ] + } + ], + "dependsOn": [ + "kart-control" + ] + }, + { + "id": "item-projectiles", + "name": "Item projectiles", + "purpose": "Flight and steering of thrown and dropped items: gravity and ground bounce for rolling shells, a phased search-and-attack steering loop for the homing shell, star pickups that hover and rotate, and the protective heart that eats hits", + "paths": [ + "src/Sato/ItemGTurtle.cpp", + "src/Sato/ItemFlyTurtle.cpp", + "src/Sato/ItemStar.cpp", + "src/Sato/ItemHeart.cpp" + ], + "functions": [ + { + "name": "ItemGTurtle::doUnivInitRelease", + "path": "src/Sato/ItemGTurtle.cpp", + "lines": [ + 187, + 215 + ] + }, + { + "name": "ItemGTurtle::setInitReleaseParam", + "path": "src/Sato/ItemGTurtle.cpp", + "lines": [ + 217, + 238 + ] + }, + { + "name": "ItemGTurtle::doUnivRelease", + "path": "src/Sato/ItemGTurtle.cpp", + "lines": [ + 251, + 287 + ] + }, + { + "name": "ItemGTurtle::chkColWallDisappear", + "path": "src/Sato/ItemGTurtle.cpp", + "lines": [ + 318, + 342 + ] + }, + { + "name": "ItemGTurtle::moveNormal", + "path": "src/Sato/ItemGTurtle.cpp", + "lines": [ + 344, + 393 + ] + }, + { + "name": "ItemFlyTurtle::moveSearch", + "path": "src/Sato/ItemFlyTurtle.cpp", + "lines": [ + 361, + 407 + ] + }, + { + "name": "ItemFlyTurtle::moveAttack", + "path": "src/Sato/ItemFlyTurtle.cpp", + "lines": [ + 409, + 443 + ] + }, + { + "name": "ItemFlyTurtle::moveCommon", + "path": "src/Sato/ItemFlyTurtle.cpp", + "lines": [ + 445, + 465 + ] + }, + { + "name": "ItemFlyTurtle::find1StKartNo", + "path": "src/Sato/ItemFlyTurtle.cpp", + "lines": [ + 475, + 512 + ] + }, + { + "name": "ItemStar::reset", + "path": "src/Sato/ItemStar.cpp", + "lines": [ + 17, + 33 + ] + }, + { + "name": "ItemHeart::setHitItem", + "path": "src/Sato/ItemHeart.cpp", + "lines": [ + 69, + 118 + ] + } + ], + "dependsOn": [ + "item-system", + "course-terrain" + ] + }, + { + "id": "traffic-cars", + "name": "Course traffic cars", + "purpose": "Ambient road traffic that follows authored path points, accelerates and damps itself, and switches into damage, recover, death or wait states when a kart or an item hits it", + "paths": [ + "src/Kaneshige/GeoCar.cpp" + ], + "functions": [ + { + "name": "GeoCar::calc", + "path": "src/Kaneshige/GeoCar.cpp", + "lines": [ + 362, + 389 + ] + }, + { + "name": "GeoCar::resetDrivePower", + "path": "src/Kaneshige/GeoCar.cpp", + "lines": [ + 326, + 330 + ] + }, + { + "name": "GeoCar::checkKartCollision", + "path": "src/Kaneshige/GeoCar.cpp", + "lines": [ + 405, + 415 + ] + }, + { + "name": "GeoCar::hitKart", + "path": "src/Kaneshige/GeoCar.cpp", + "lines": [ + 417, + 422 + ] + } + ], + "dependsOn": [ + "course-terrain" + ] + }, + { + "id": "kart-ai", + "name": "CPU kart orchestration", + "purpose": "Per-frame order of the CPU driver's body, speed and item controllers, and the rank array the CPU logic reads; the steering, speed and item brains themselves are not written at this commit", + "paths": [ + "src/Sato/RivalKart.cpp" + ], + "functions": [ + { + "name": "RivalKart::setRankAry", + "path": "src/Sato/RivalKart.cpp", + "lines": [ + 68, + 72 + ] + }, + { + "name": "RivalKart::getMaxSpeed", + "path": "src/Sato/RivalKart.cpp", + "lines": [ + 74, + 77 + ] + }, + { + "name": "RivalKart::ContempLate", + "path": "src/Sato/RivalKart.cpp", + "lines": [ + 89, + 100 + ] + } + ], + "dependsOn": [ + "kart-control", + "race-flow" + ] + } + ], + "cards": [ + { + "id": "kart-box-inertia", + "node": "kart-body", + "name": "Box mass and inertia", + "summary": "Each kart body gets its mass and diagonal inertia from the box it is shaped like, so heavier and longer bodies resist turning differently", + "behavior": "Body setup takes the four half-extents of the kart's box and builds two numbers from them. The mass is the product of all four extents scaled by eight, which is the volume of a box whose sides are twice those extents. That mass is then divided by twelve and multiplied by four to give an inertia coefficient, and each axis of the inertia diagonal is that coefficient weighted by the other two extents squared. The result is a solid-box inertia tensor kept as a diagonal, so a body that is long in one direction resists yaw more than one that is tall. The contact solver later inverts those diagonal terms into a full three by three mass matrix at the contact offset, so the same numbers that decide the mass decide how much a hit spins a kart instead of shoving it. Because the mass is derived rather than authored, a kart that has been shrunk keeps a proportional inertia and still turns like a small version of itself.", + "constants": [ + { + "name": "boxVolumeFactor", + "value": 8, + "unit": "dimensionless volume multiplier, applied once per body setup", + "source": "src/Yamamoto/kartBody.cpp:268" + }, + { + "name": "inertiaMassDivisor", + "value": 12, + "unit": "dimensionless divisor, applied once per body setup", + "source": "src/Yamamoto/kartBody.cpp:270" + }, + { + "name": "inertiaAxisFactor", + "value": 4, + "unit": "dimensionless multiplier, applied once per body setup", + "source": "src/Yamamoto/kartBody.cpp:270" + }, + { + "name": "inverseMassUnit", + "value": 1, + "unit": "dimensionless reciprocal, recomputed every contact solve", + "source": "src/Yamamoto/kartBody.cpp:582" + } + ], + "sources": [ + { + "path": "src/Yamamoto/kartBody.cpp", + "line": 267 + }, + { + "path": "src/Yamamoto/kartBody.cpp", + "line": 268 + }, + { + "path": "src/Yamamoto/kartBody.cpp", + "line": 270 + }, + { + "path": "src/Yamamoto/kartBody.cpp", + "line": 578 + } + ], + "capabilities": [ + "box mass derivation", + "diagonal inertia tensor", + "mass matrix at contact offset" + ], + "tags": [ + "physics", + "mass", + "inertia", + "rigid-body" + ] + }, + { + "id": "kart-gravity-selection", + "node": "kart-body", + "name": "Gravity selection and slope assist", + "summary": "Four gravity values are picked from the kart's state flags, and part of that gravity is redirected along the ground normal to keep fast karts planted", + "behavior": "Every update the body asks which gravity to use. A kart in a knocked-about state gets the strongest pull, a kart flagged as launching or hovering uses a lighter one, other temporary states use a third value, and everything else falls back to the value stored on the body. The gravity is multiplied by the mass before it becomes a vertical force, so the two are never applied separately. A separate assist fires when the kart is moving fast, its own up axis has tilted past a small dot-product threshold and at least one wheel is touching: instead of pulling straight down, a fraction of gravity is pushed back along the body's up axis, which is what stops a strongly banked kart from peeling off the surface. A lighter version of the same assist, horizontal only, is added for a kart flagged as airborne but still in contact. The accumulated steering torque buffers are cleared whenever the assist fires, so a stale input does not fight the correction.", + "constants": [ + { + "name": "knockedGravity", + "value": -18, + "unit": "world units per update of vertical acceleration, applied every physics update", + "source": "src/Yamamoto/kartBody.cpp:437" + }, + { + "name": "lightGravity", + "value": -11.8, + "unit": "world units per update of vertical acceleration, applied every physics update", + "source": "src/Yamamoto/kartBody.cpp:435" + }, + { + "name": "stateGravity", + "value": -13.5, + "unit": "world units per update of vertical acceleration, applied every physics update", + "source": "src/Yamamoto/kartBody.cpp:439" + }, + { + "name": "slopeAssistShare", + "value": 0.3, + "unit": "fraction of gravity redirected per update", + "source": "src/Yamamoto/kartBody.cpp:448" + }, + { + "name": "slopeAssistSpeedGate", + "value": 60, + "unit": "world units of speed, compared every physics update", + "source": "src/Yamamoto/kartBody.cpp:447" + }, + { + "name": "slopeAssistUpDotGate", + "value": 0.705, + "unit": "unitless dot product with world up, sampled every physics update", + "source": "src/Yamamoto/kartBody.cpp:447" + } + ], + "sources": [ + { + "path": "src/Yamamoto/kartBody.cpp", + "line": 429 + }, + { + "path": "src/Yamamoto/kartBody.cpp", + "line": 444 + }, + { + "path": "src/Yamamoto/kartBody.cpp", + "line": 447 + }, + { + "path": "src/Yamamoto/kartBody.cpp", + "line": 463 + } + ], + "capabilities": [ + "state driven gravity", + "ground-normal assist", + "banked driving" + ], + "tags": [ + "physics", + "gravity", + "slope", + "feel" + ] + }, + { + "id": "kart-force-integration", + "node": "kart-body", + "name": "Force and torque integration", + "summary": "Accumulated forces become linear velocity, accumulated torque becomes angular velocity through a half-step split, and the result is damped before it moves the pose", + "behavior": "Forces gathered during the update are divided by the body mass and added straight into velocity. Torque is handled differently: it is rotated into body space, solved against the inertia diagonal, split by half into the angular velocity, then the second half is applied after a correction term, which is the usual way to keep a semi-implicit integrator stable when angular rates are large. A control step then damps the angular velocity by a fixed fraction unless the kart is in one of the states that must not be damped. The angular velocity is converted back to world space, the force and torque buffers are zeroed for the next update, and the pose matrix is rebuilt from the body vectors so the render transform follows the physics rather than the input. Because the damping is applied once per update rather than scaled by elapsed time, the settle time after a wall hit is fixed to the update rate.", + "constants": [ + { + "name": "torqueHalfStep", + "value": 2, + "unit": "dimensionless divisor per physics update", + "source": "src/Yamamoto/kartBody.cpp:497" + }, + { + "name": "angularDampingFraction", + "value": 0.523333, + "unit": "fraction of angular velocity kept per physics update", + "source": "src/Yamamoto/kartBody.cpp:514" + } + ], + "sources": [ + { + "path": "src/Yamamoto/kartBody.cpp", + "line": 472 + }, + { + "path": "src/Yamamoto/kartBody.cpp", + "line": 481 + }, + { + "path": "src/Yamamoto/kartBody.cpp", + "line": 514 + }, + { + "path": "src/Yamamoto/kartBody.cpp", + "line": 527 + } + ], + "capabilities": [ + "semi-implicit angular integration", + "rate damping", + "pose write-back" + ], + "tags": [ + "physics", + "integration", + "rotation", + "feel" + ] + }, + { + "id": "kart-ground-impulse", + "node": "kart-contact", + "name": "Ground impulse and rolling friction", + "summary": "A wheel touching the ground receives an impulse along the contact normal, capped in the vertical direction, plus a friction impulse clamped inside a cone", + "behavior": "For a wheel contact the code measures the approach speed along the surface normal, subtracts a friction share of it, and only pushes when the remainder is positive. The push follows the normal, except that its vertical component is held under a cap unless the kart is in a state that wants a bigger pop, which is what makes a hard landing bounce slightly instead of launching. When the contact also carries a friction coefficient, the solver builds the contact mass matrix at the wheel offset, inverts it and solves for the impulse that cancels tangential slip, then clamps that impulse inside a circle whose radius is the coefficient squared, so a slippery surface simply cannot deliver a large tangential push. Velocity and angular velocity are both nudged by the applied impulse and both are scaled slightly up afterwards, which keeps a kart from sinking into a seam between polygons. The same routine finishes by recomputing the kart's speed control target from its top speed divided by a little over two, so a collision bleeds the target the throttle is chasing rather than the raw input.", + "constants": [ + { + "name": "verticalImpulseCap", + "value": 65, + "unit": "world units per update of vertical impulse, applied per contact", + "source": "src/Yamamoto/kartBody.cpp:670" + }, + { + "name": "postContactScale", + "value": 1.01, + "unit": "dimensionless multiplier per contact", + "source": "src/Yamamoto/kartBody.cpp:687" + }, + { + "name": "dryGroundFrictionShare", + "value": 0.1, + "unit": "fraction of normal approach speed removed per contact", + "source": "src/Yamamoto/kartBody.cpp:1228" + }, + { + "name": "slickGroundFrictionShare", + "value": 0.01, + "unit": "fraction of normal approach speed removed per contact", + "source": "src/Yamamoto/kartBody.cpp:1225" + }, + { + "name": "contactSpeedDivisor", + "value": 2.16, + "unit": "dimensionless divisor per contact", + "source": "src/Yamamoto/kartBody.cpp:690" + } + ], + "sources": [ + { + "path": "src/Yamamoto/kartBody.cpp", + "line": 638 + }, + { + "path": "src/Yamamoto/kartBody.cpp", + "line": 668 + }, + { + "path": "src/Yamamoto/kartBody.cpp", + "line": 682 + }, + { + "path": "src/Yamamoto/kartBody.cpp", + "line": 1216 + } + ], + "capabilities": [ + "normal impulse", + "friction cone clamp", + "seam jitter suppression", + "surface dependent grip" + ], + "tags": [ + "physics", + "collision", + "friction", + "ground" + ] + }, + { + "id": "kart-wall-impact", + "node": "kart-contact", + "name": "Wall reflection and impact windows", + "summary": "A wall pushes the kart back along its normal, and whether that counts as a head-on or a glancing hit depends on speed and on the angle between the hull and the wall", + "behavior": "Wall handling reflects the kart's velocity across the wall normal and applies the result at the contact point, so an off-centre hit also spins the kart. Above a speed threshold and an angle threshold the hit raises an impact effect, and the same angle is divided by a fixed value to drive how loud that impact sounds. The angle between the kart's forward axis and the wall normal is then compared against two windows: while the kart is moving fast only impacts inside a narrower pair of bounds count as a genuine head-on, and once the kart has slowed the window widens. The narrow window is roughly sixty to a hundred and twenty degrees and the wide one roughly twenty to a hundred and sixty. That widening is why a slow scrape along a barrier still registers while the same contact at speed on a straight does not, and it is also what stops repeated retriggering when a kart is pinned against geometry.", + "constants": [ + { + "name": "wallEffectSpeedGate", + "value": 30, + "unit": "world units of speed, compared every physics update", + "source": "src/Yamamoto/kartBody.cpp:1248" + }, + { + "name": "wallEffectAngleGate", + "value": 15, + "unit": "degrees, compared every physics update", + "source": "src/Yamamoto/kartBody.cpp:1248" + }, + { + "name": "impactLoudnessDivisor", + "value": 120, + "unit": "degrees per unit, evaluated per impact", + "source": "src/Yamamoto/kartBody.cpp:1254" + }, + { + "name": "headOnWindowLow", + "value": 1.046666, + "unit": "radians, evaluated per impact", + "source": "src/Yamamoto/kartBody.cpp:1268" + }, + { + "name": "headOnWindowHigh", + "value": 2.0933299, + "unit": "radians, evaluated per impact", + "source": "src/Yamamoto/kartBody.cpp:1268" + }, + { + "name": "slowWindowLow", + "value": 0.348888, + "unit": "radians, evaluated per impact", + "source": "src/Yamamoto/kartBody.cpp:1272" + }, + { + "name": "slowWindowHigh", + "value": 2.79111, + "unit": "radians, evaluated per impact", + "source": "src/Yamamoto/kartBody.cpp:1272" + } + ], + "sources": [ + { + "path": "src/Yamamoto/kartBody.cpp", + "line": 1178 + }, + { + "path": "src/Yamamoto/kartBody.cpp", + "line": 1237 + }, + { + "path": "src/Yamamoto/kartBody.cpp", + "line": 1254 + }, + { + "path": "src/Yamamoto/kartBody.cpp", + "line": 1268 + } + ], + "capabilities": [ + "wall normal reflection", + "speed gated impact effects", + "angle windows for hit classification" + ], + "tags": [ + "collision", + "wall", + "impact", + "feel" + ] + }, + { + "id": "kart-versus-kart", + "node": "kart-contact", + "name": "Kart versus kart bumps", + "summary": "A heavier kart shoves a lighter one harder, invulnerable karts multiply their shove, and side-on hits trade a large horizontal impulse for a tiny vertical one", + "behavior": "Before any bounce is computed the code decides whether one kart should simply win the exchange, testing an ordered list of states: invulnerability, accumulated damage, a grab state, a shrink state and a scale state. If no winner is found, the two bodies' mass terms are divided into a pair of ratios, each floored at one and raised by half again when it exceeds one, so the heavier kart always carries a multiplier. Two special cases override the ratios with large fixed values, one for an invulnerable kart and one for a shrunken kart, and both bypass the ordinary path. When the contact is side-on, meaning the impact vector sits inside a band around perpendicular, the karts receive a strike impulse proportional to the mass ratio times a base value, capped, with only a tenth of it applied vertically, which is what makes a side check push a kart sideways rather than popping it up. Otherwise the impulse is the closing speed minus a friction share, floored so that even a nudge registers, and the rumble strength is quantised into twelve steps so the pad feedback tracks the impulse coarsely rather than buzzing continuously.", + "constants": [ + { + "name": "massRatioBoost", + "value": 1.5, + "unit": "dimensionless multiplier per bump", + "source": "src/Yamamoto/kartBody.cpp:769" + }, + { + "name": "invulnerableRatio", + "value": 3, + "unit": "dimensionless multiplier per bump", + "source": "src/Yamamoto/kartBody.cpp:780" + }, + { + "name": "sideHitAngleLow", + "value": 45, + "unit": "degrees, evaluated per bump", + "source": "src/Yamamoto/kartBody.cpp:891" + }, + { + "name": "sideHitAngleHigh", + "value": 135, + "unit": "degrees, evaluated per bump", + "source": "src/Yamamoto/kartBody.cpp:891" + }, + { + "name": "strikeImpulseBase", + "value": 130, + "unit": "world units per update per mass ratio, applied per bump", + "source": "src/Yamamoto/kartBody.cpp:893" + }, + { + "name": "strikeImpulseCap", + "value": 300, + "unit": "world units per update, applied per bump", + "source": "src/Yamamoto/kartBody.cpp:895" + }, + { + "name": "strikeVerticalShare", + "value": 0.1, + "unit": "fraction of the strike applied vertically per bump", + "source": "src/Yamamoto/kartBody.cpp:898" + }, + { + "name": "minimumBumpImpulse", + "value": 3, + "unit": "world units per update, applied per bump", + "source": "src/Yamamoto/kartBody.cpp:927" + }, + { + "name": "bumpRumbleDivisor", + "value": 3, + "unit": "world units per update per rumble step, applied per bump", + "source": "src/Yamamoto/kartBody.cpp:930" + } + ], + "sources": [ + { + "path": "src/Yamamoto/kartBody.cpp", + "line": 743 + }, + { + "path": "src/Yamamoto/kartBody.cpp", + "line": 837 + }, + { + "path": "src/Yamamoto/kartBody.cpp", + "line": 889 + }, + { + "path": "src/Yamamoto/kartBody.cpp", + "line": 930 + } + ], + "capabilities": [ + "mass weighted bouncing", + "state based shove priority", + "side check impulse", + "quantised rumble" + ], + "tags": [ + "combat", + "collision", + "bumping", + "feel" + ] + }, + { + "id": "kart-smoothing-primitives", + "node": "kart-math", + "name": "Solver and smoothing primitives", + "summary": "The shared numeric helpers: a three by three elimination solve, a fast reciprocal square root, and ramps that chase a target with a fixed fraction or an eased one", + "behavior": "Contact resolution needs to solve a small dense system every time a wheel or a kart touches something, so the maths layer wraps a pivoting decomposition with forward and back substitution behind a single call, always at order three. Square roots go through the hardware reciprocal estimate multiplied back through the value, which is fast and accurate enough for impulse magnitudes. Angle work converts a cartesian offset into a length-elevation-azimuth triple, and the reverse direction feeds the angle through a sine and cosine table indexed by scaling a degree value into a quarter-turn table size. Two smoothing helpers are used wherever a value has to chase a target: one adds a fixed fraction of the remaining error and snaps to zero inside a tiny dead zone, while the other eases with a fraction that grows as the fourth power of elapsed steps, so the beginning of a transition is soft and the end is quick.", + "constants": [ + { + "name": "solverOrder", + "value": 3, + "unit": "matrix dimension, per solve", + "source": "src/Yamamoto/kartvec.cpp:268" + }, + { + "name": "smoothingDeadZone", + "value": 0.001, + "unit": "value units, applied every smoothing update", + "source": "src/Yamamoto/kartvec.cpp:330" + }, + { + "name": "angleTableSize", + "value": 16384, + "unit": "table entries per quarter turn, applied per conversion", + "source": "src/Yamamoto/kartvec.cpp:140" + }, + { + "name": "halfTurnDegrees", + "value": 180, + "unit": "degrees per conversion", + "source": "src/Yamamoto/kartvec.cpp:140" + }, + { + "name": "quarterTurnDegrees", + "value": 90, + "unit": "degrees per conversion", + "source": "src/Yamamoto/kartvec.cpp:140" + }, + { + "name": "piApproximation", + "value": 3.141, + "unit": "radians per half turn, used in the conversion", + "source": "src/Yamamoto/kartvec.cpp:140" + } + ], + "sources": [ + { + "path": "src/Yamamoto/kartvec.cpp", + "line": 261 + }, + { + "path": "src/Yamamoto/kartvec.cpp", + "line": 326 + }, + { + "path": "src/Yamamoto/kartvec.cpp", + "line": 347 + }, + { + "path": "src/Yamamoto/kartvec.cpp", + "line": 132 + } + ], + "capabilities": [ + "dense solve", + "fast inverse square root", + "polar conversion", + "eased chasing" + ], + "tags": [ + "math", + "solver", + "smoothing", + "utility" + ] + }, + { + "id": "kart-surface-top-speed", + "node": "kart-control", + "name": "Surface dependent top speed", + "summary": "Top speed is chosen from the surface under the wheels, and wading or splashing through water cuts it hard", + "behavior": "Top speed is not one number per kart but a lookup on the surface attribute under the body: each attribute maps to a different authored speed ceiling held on the kart, so the same driver has a different ceiling on tarmac, on a loose surface and on a low-grip surface. Two modifiers sit on top of the chosen ceiling. If the surface reports a splash height and the kart's hull is below it, the ceiling is scaled down, which is what makes a puddle or a shallow flooded stretch cost speed. If the surface is water the ceiling is compared against the water line: a kart barely submerged keeps most of its speed, while a kart submerged past a deep threshold keeps less than a third of it. Because both the surface attribute and the water line are re-read every update, the penalty appears and clears as the kart crosses an edge instead of being latched at entry.", + "constants": [ + { + "name": "splashSpeedScale", + "value": 0.7, + "unit": "multiplier on top speed, applied every update", + "source": "src/Yamamoto/kartCtrlInfo.cpp:442" + }, + { + "name": "shallowWaterSpeedScale", + "value": 0.7, + "unit": "multiplier on top speed, applied every update", + "source": "src/Yamamoto/kartCtrlInfo.cpp:471" + }, + { + "name": "deepWaterSpeedScale", + "value": 0.3, + "unit": "multiplier on top speed, applied every update", + "source": "src/Yamamoto/kartCtrlInfo.cpp:468" + }, + { + "name": "deepWaterDepthGate", + "value": -50, + "unit": "world units below the water line, sampled every update", + "source": "src/Yamamoto/kartCtrlInfo.cpp:467" + } + ], + "sources": [ + { + "path": "src/Yamamoto/kartCtrlInfo.cpp", + "line": 432 + }, + { + "path": "src/Yamamoto/kartCtrlInfo.cpp", + "line": 436 + }, + { + "path": "src/Yamamoto/kartCtrlInfo.cpp", + "line": 464 + } + ], + "capabilities": [ + "per surface speed ceiling", + "splash penalty", + "water depth penalty" + ], + "tags": [ + "movement", + "speed", + "surface", + "water" + ] + }, + { + "id": "kart-down-slope", + "node": "kart-control", + "name": "Down-slope bonus", + "summary": "Pointing downhill past a small dead zone adds acceleration, and past a larger angle it also raises the speed ceiling, both saturating at a clamp angle", + "behavior": "The kart's pitch relative to world up becomes a signed number whose magnitude is the slope angle. Only negative values, meaning the nose points downhill while the kart is not perfectly flat, earn anything. For acceleration the angle is first compared against a small dead zone, under which the multiplier is exactly one; past it the angle is clamped at a much larger value and remapped into a fraction, and the returned multiplier is that fraction scaled by a fifth and added to one. For speed a second, smaller pair of thresholds applies: under the lower one the bonus is zero, at the clamp it reaches the maximum, and the bonus is linear in between. The two use different angles on purpose, so a gentle down-slope that already helps a kart accelerate has not yet started raising its ceiling. Both are pure functions of the current pose, so neither lags the moment the kart crests a hill.", + "constants": [ + { + "name": "slopeAccDeadZone", + "value": 0.0872222, + "unit": "radians, sampled every update", + "source": "src/Yamamoto/kartCtrlInfo.cpp:490" + }, + { + "name": "slopeAccClampAngle", + "value": 0.785, + "unit": "radians, sampled every update", + "source": "src/Yamamoto/kartCtrlInfo.cpp:492" + }, + { + "name": "slopeAccBonus", + "value": 0.2, + "unit": "multiplier added at full clamp, evaluated every update", + "source": "src/Yamamoto/kartCtrlInfo.cpp:497" + }, + { + "name": "slopeSpeedDeadZone", + "value": 0.0523333, + "unit": "radians, sampled every update", + "source": "src/Yamamoto/kartCtrlInfo.cpp:509" + }, + { + "name": "slopeSpeedClampAngle", + "value": 0.174444, + "unit": "radians, sampled every update", + "source": "src/Yamamoto/kartCtrlInfo.cpp:511" + }, + { + "name": "slopeSpeedBonus", + "value": 3, + "unit": "world units per update added at full clamp", + "source": "src/Yamamoto/kartCtrlInfo.cpp:515" + } + ], + "sources": [ + { + "path": "src/Yamamoto/kartCtrlInfo.cpp", + "line": 484 + }, + { + "path": "src/Yamamoto/kartCtrlInfo.cpp", + "line": 503 + }, + { + "path": "src/Yamamoto/kartCtrlInfo.cpp", + "line": 490 + }, + { + "path": "src/Yamamoto/kartCtrlInfo.cpp", + "line": 509 + } + ], + "capabilities": [ + "pitch driven acceleration bonus", + "speed bonus on steeper slopes", + "dead zones" + ], + "tags": [ + "movement", + "slope", + "acceleration", + "feel" + ] + }, + { + "id": "kart-spin-crash", + "node": "kart-crash", + "name": "Full and half spin-outs", + "summary": "A spin target angle is chosen from the side the hit came from, then the angular rate ramps up and eases as the body reaches two turns or one", + "behavior": "Both spin variants read the lateral part of the accumulated impulse to pick which way to rotate, fall back to the steering angle when that is exactly zero, and then enter a state that drives the yaw rate towards a full turn target. The full spin aims at two complete turns, the half spin at one. While the accumulated angle is small the rate gains a large increment per update; once it passes a coarse threshold the increments shrink, so the tail of the spin settles onto the target instead of slamming into it. When the accumulated angle passes a completion threshold slightly under the target the wheel angular rates are zeroed, the spin flags are cleared and the kart returns to driving. Because the target is an absolute angle rather than a timer, a kart nudged into a wall mid-spin keeps the rotation it has already banked, and the same routine serves both the ordinary spin and the shorter half spin, which is why the half spin reaches its threshold and unwinds sooner.", + "constants": [ + { + "name": "fullSpinTarget", + "value": 12.56, + "unit": "radians per spin state", + "source": "src/Yamamoto/KartCrash.cpp:481" + }, + { + "name": "fullSpinComplete", + "value": 12.55, + "unit": "radians, checked every update", + "source": "src/Yamamoto/KartCrash.cpp:490" + }, + { + "name": "halfSpinTarget", + "value": 6.28, + "unit": "radians per spin state", + "source": "src/Yamamoto/KartCrash.cpp:553" + }, + { + "name": "halfSpinComplete", + "value": 6.17, + "unit": "radians, checked every update", + "source": "src/Yamamoto/KartCrash.cpp:562" + }, + { + "name": "spinRampLate", + "value": 0.0697777, + "unit": "radians per update, ramped every update", + "source": "src/Yamamoto/KartCrash.cpp:477" + }, + { + "name": "spinRampEarly", + "value": 0.174444, + "unit": "radians per update, ramped every update", + "source": "src/Yamamoto/KartCrash.cpp:479" + }, + { + "name": "halfSpinRampLate", + "value": 0.1046666, + "unit": "radians per update, ramped every update", + "source": "src/Yamamoto/KartCrash.cpp:549" + }, + { + "name": "spinIncrementFine", + "value": 0.00348888, + "unit": "radians per update, stepped every update", + "source": "src/Yamamoto/KartCrash.cpp:477" + }, + { + "name": "spinDirectionSeed", + "value": -0.0174444, + "unit": "radians per update, set on spin entry", + "source": "src/Yamamoto/KartCrash.cpp:439" + } + ], + "sources": [ + { + "path": "src/Yamamoto/KartCrash.cpp", + "line": 430 + }, + { + "path": "src/Yamamoto/KartCrash.cpp", + "line": 470 + }, + { + "path": "src/Yamamoto/KartCrash.cpp", + "line": 503 + }, + { + "path": "src/Yamamoto/KartCrash.cpp", + "line": 542 + } + ], + "capabilities": [ + "directional spin", + "angular ramp", + "angle based recovery" + ], + "tags": [ + "combat", + "crash", + "spin", + "recovery" + ] + }, + { + "id": "kart-launch-crash", + "node": "kart-crash", + "name": "Thrown, bombed and frozen", + "summary": "Hazards that remove control launch the kart upward with an impulse scaled by the hazard's power, and each hazard type has its own recovery timing", + "behavior": "A thrower-style hazard reports a power value; the crash routine multiplies it by two hundred and by the kart's mass and injects that straight into the vertical force buffer, so a heavy kart is thrown as high as a light one rather than less. The same entry point seeds a small negative pitch rate, zeroes the spin and slope buffers, raises the launched flag and marks the kart as crashing so steering input is ignored. A bomb does the same with a much larger fixed vertical impulse and a positive pitch rate, which is why a bomb sends a kart up and forward rather than up and back. The freeze variant takes a different route: it stops the driver animation outright, raises a frozen flag, aborts any item roulette the victim was spinning and shuts off their exhaust effect for the duration before unwinding through its own state machine. Every entry point also starts a short grace timer so the victim cannot be hit again the moment it lands.", + "constants": [ + { + "name": "throwerImpulseScale", + "value": 200, + "unit": "world units per update per unit of hazard power", + "source": "src/Yamamoto/KartCrash.cpp:320" + }, + { + "name": "bombImpulse", + "value": 600, + "unit": "world units per update of vertical impulse", + "source": "src/Yamamoto/KartCrash.cpp:243" + }, + { + "name": "launchPitchSeed", + "value": -0.0174444, + "unit": "radians per update, set on launch", + "source": "src/Yamamoto/KartCrash.cpp:327" + }, + { + "name": "bombPitchSeed", + "value": 0.174444, + "unit": "radians per update, set on launch", + "source": "src/Yamamoto/KartCrash.cpp:245" + }, + { + "name": "launchedStateId", + "value": 15, + "unit": "state index, set on launch", + "source": "src/Yamamoto/KartCrash.cpp:329" + }, + { + "name": "bombStateId", + "value": 6, + "unit": "state index, set on launch", + "source": "src/Yamamoto/KartCrash.cpp:237" + }, + { + "name": "frozenStateId", + "value": 18, + "unit": "state index, set on freeze", + "source": "src/Yamamoto/KartCrash.cpp:612" + } + ], + "sources": [ + { + "path": "src/Yamamoto/KartCrash.cpp", + "line": 309 + }, + { + "path": "src/Yamamoto/KartCrash.cpp", + "line": 233 + }, + { + "path": "src/Yamamoto/KartCrash.cpp", + "line": 608 + }, + { + "path": "src/Yamamoto/KartCrash.cpp", + "line": 351 + } + ], + "capabilities": [ + "hazard impulse scaling", + "control lockout", + "freeze with effect teardown", + "post hit grace" + ], + "tags": [ + "combat", + "crash", + "hazard", + "recovery" + ] + }, + { + "id": "kart-star-invulnerability", + "node": "kart-status", + "name": "Star invulnerability window", + "summary": "A star grants a fixed countdown of invulnerability that is shorter in the escape battle mode", + "behavior": "Entering the star state raises an invulnerable flag on the body, plays a state change on the kart's material animation and starts a countdown. The countdown takes one of two fixed lengths: the shorter value is used in the escape battle mode where a tighter power window keeps the round moving, and the longer value everywhere else. Each update simply decrements, and when it reaches zero the flag is cleared and the material animation is stopped, returning the kart to ordinary collision rules. During the window the kart counts as a winner in kart-versus-kart contacts, so the mass ratio logic hands it a much larger shove and it never receives one itself, and it also ignores item reactions that would otherwise spin it.", + "constants": [ + { + "name": "starFramesNormal", + "value": 450, + "unit": "frames at 60 Hz", + "source": "src/Yamamoto/KartStar.cpp:24" + }, + { + "name": "starFramesEscape", + "value": 360, + "unit": "frames at 60 Hz", + "source": "src/Yamamoto/KartStar.cpp:22" + } + ], + "sources": [ + { + "path": "src/Yamamoto/KartStar.cpp", + "line": 15 + }, + { + "path": "src/Yamamoto/KartStar.cpp", + "line": 24 + }, + { + "path": "src/Yamamoto/KartStar.cpp", + "line": 40 + } + ], + "capabilities": [ + "timed invulnerability", + "mode specific duration", + "material state change" + ], + "tags": [ + "combat", + "power-up", + "invulnerability", + "timing" + ] + }, + { + "id": "kart-thunder-shrink", + "node": "kart-status", + "name": "Thunder shrink and recovery", + "summary": "The thunder item shrinks every other kart to half scale for a fixed window, then a two stage recovery shakes it back to full size", + "behavior": "When the item fires it walks every kart in the race and skips any that is already shrunken, invulnerable, inside a cannon or otherwise immune. Each valid target is flagged, force spun, made to drop its items, given a rumble motor and a voice cue, and put on a countdown starting from a shake scale of one. During the shrink phase the scale eases towards a half, and once the countdown has burned all but the final half second the state moves to waiting. At zero the recovery cue plays and the scale eases back towards one; the shake phase then decays a tiny amplitude to nothing while a sine wave with a fixed increment drives the scale around one, and only when the amplitude is exhausted is the flag cleared, the shadow restored and the kart handed back to normal rules. The same object exposes a scale that the physics layer multiplies the inertia diagonal by, so a shrunken kart both looks and spins like a smaller body.", + "constants": [ + { + "name": "thunderFrames", + "value": 540, + "unit": "frames at 60 Hz", + "source": "src/Yamamoto/KartThunder.cpp:70" + }, + { + "name": "shrinkPhaseSwitch", + "value": 510, + "unit": "frames at 60 Hz counted down, checked every update", + "source": "src/Yamamoto/KartThunder.cpp:82" + }, + { + "name": "shrunkenScale", + "value": 0.5, + "unit": "dimensionless scale, applied every update", + "source": "src/Yamamoto/KartThunder.cpp:81" + }, + { + "name": "shrinkStep", + "value": 0.03, + "unit": "scale units per update", + "source": "src/Yamamoto/KartThunder.cpp:81" + }, + { + "name": "recoverStep", + "value": 0.2, + "unit": "scale units per update", + "source": "src/Yamamoto/KartThunder.cpp:19" + }, + { + "name": "shakeDecayStep", + "value": 0.006, + "unit": "amplitude units per update", + "source": "src/Yamamoto/KartThunder.cpp:28" + }, + { + "name": "shakePhaseStep", + "value": 0.680333, + "unit": "radians per update", + "source": "src/Yamamoto/KartThunder.cpp:29" + } + ], + "sources": [ + { + "path": "src/Yamamoto/KartThunder.cpp", + "line": 48 + }, + { + "path": "src/Yamamoto/KartThunder.cpp", + "line": 70 + }, + { + "path": "src/Yamamoto/KartThunder.cpp", + "line": 76 + }, + { + "path": "src/Yamamoto/KartThunder.cpp", + "line": 27 + } + ], + "capabilities": [ + "mass shrink", + "staged recovery", + "shake animation", + "immunity filtering" + ], + "tags": [ + "combat", + "status", + "size", + "timing" + ] + }, + { + "id": "kart-damage-lockout", + "node": "kart-status", + "name": "Damage lockout timers", + "summary": "Four damage reactions each hold the driver for a different number of frames, and the two biggest ones also shake the camera", + "behavior": "Reactions are chosen from the severity of the hit: a short one for ordinary bumps, a medium one for half strength hits, a long one for burns, and the longest for the big hit that follows a bomb or a heavy impact. Each sets a state and a countdown; while the countdown runs the driver animation is forced into the reaction and steering input is ignored. When it expires the reaction flag is cleared and a release animation plays, so the kart visibly settles before control comes back. The two larger reactions also ask the camera looking at that kart for a shake, which is why a bomb reads as a big event even when the victim is off to the side of the screen. A protection window suppresses all of it, so a kart that is already in one does not accumulate further lockouts.", + "constants": [ + { + "name": "lightLockoutFrames", + "value": 20, + "unit": "frames at 60 Hz", + "source": "src/Yamamoto/KartDamage.cpp:252" + }, + { + "name": "halfLockoutFrames", + "value": 60, + "unit": "frames at 60 Hz", + "source": "src/Yamamoto/KartDamage.cpp:244" + }, + { + "name": "burnLockoutFrames", + "value": 90, + "unit": "frames at 60 Hz", + "source": "src/Yamamoto/KartDamage.cpp:270" + }, + { + "name": "bigLockoutFrames", + "value": 300, + "unit": "frames at 60 Hz", + "source": "src/Yamamoto/KartDamage.cpp:261" + }, + { + "name": "cameraShakeStrength", + "value": 0.4, + "unit": "shake amplitude units, set once per big hit", + "source": "src/Yamamoto/KartDamage.cpp:155" + } + ], + "sources": [ + { + "path": "src/Yamamoto/KartDamage.cpp", + "line": 124 + }, + { + "path": "src/Yamamoto/KartDamage.cpp", + "line": 210 + }, + { + "path": "src/Yamamoto/KartDamage.cpp", + "line": 247 + }, + { + "path": "src/Yamamoto/KartDamage.cpp", + "line": 255 + } + ], + "capabilities": [ + "tiered lockout", + "driver animation states", + "camera shake", + "immunity suppression" + ], + "tags": [ + "combat", + "damage", + "stun", + "camera" + ] + }, + { + "id": "kart-chase-camera", + "node": "kart-camera", + "name": "Chase camera placement and mode choice", + "summary": "The per kart camera picks a mode every update and frames the kart from an authored anchor pushed along its forward direction", + "behavior": "Each update the camera resolves which of a dozen or more modes applies, testing in order whether the kart is hanging after a fall, dropping, launching, landing, entering a pipe, reversing, in a start or finish demo, in a replay, or simply driving; only the ordinary driving case reaches the free chase mode. For the authored modes the camera reads a camera record for the area the kart is in, takes the record's position and pushes it a fixed distance along the record's own forward direction, which is how over-the-shoulder and roadside shots are built from level data rather than from the kart pose. The pure chase mode instead lifts an anchor a fixed height above the kart and keeps the distance from that anchor to the authored camera, effectively rotating the authored offset with the kart. The search and path variants reuse the same record and the same push, differing only in whether the anchor starts at the record or at the kart, which is why every mode lines up with the authored framing when the camera cuts between them.", + "constants": [ + { + "name": "framingPushDistance", + "value": 100, + "unit": "world units along the record forward direction, applied per placement", + "source": "src/Yamamoto/KartPerCam.cpp:103" + }, + { + "name": "chaseAnchorHeight", + "value": 30, + "unit": "world units above the kart origin, applied per placement", + "source": "src/Yamamoto/KartPerCam.cpp:115" + } + ], + "sources": [ + { + "path": "src/Yamamoto/kartCamera.cpp", + "line": 455 + }, + { + "path": "src/Yamamoto/kartCamera.cpp", + "line": 526 + }, + { + "path": "src/Yamamoto/kartCamera.cpp", + "line": 575 + }, + { + "path": "src/Yamamoto/KartPerCam.cpp", + "line": 109 + }, + { + "path": "src/Yamamoto/KartPerCam.cpp", + "line": 45 + } + ], + "capabilities": [ + "mode selection by kart state", + "authored camera anchors", + "height offset chase", + "field of view easing" + ], + "tags": [ + "camera", + "framing", + "mode", + "feel" + ] + }, + { + "id": "kart-lap-progress", + "node": "lap-tracking", + "name": "Sector progress and lap counting", + "summary": "Position becomes a fraction between two checkpoint planes, and that fraction accumulates into race progress with wrap handling and a reverse-drive test", + "behavior": "Each sector is bounded by two checkpoint planes. A kart's planar position is tested against the sector's left and right bounds with cross products in the ground plane, and if it is inside, the fractional distance is the distance from the entry plane divided by the sum of the distances to both planes, clamped so that a hair-thin negative result collapses to zero. That fraction is scaled by the share of total track length the sector covers and offset by the track that lies before it, giving a progression within one lap. The difference from the previous update is taken, and any jump larger than half a lap is corrected by a whole lap in the appropriate direction, which is what makes crossing the start line and driving back over it both behave sensibly. Accumulated race progress is floored at a negative limit so a kart driving the wrong way for a long time cannot underflow the comparison used for ranking. A lap only completes when the sector pass set is complete and the accumulated progress has exceeded the recorded lap count, so cutting across the infield no longer counts. Reverse driving is detected independently by dotting the kart's forward axis against the sector's backward normal.", + "constants": [ + { + "name": "lapWrapNegativeThreshold", + "value": -0.5, + "unit": "fraction of a lap per update", + "source": "src/Kaneshige/KartChecker.cpp:459" + }, + { + "name": "lapWrapCorrection", + "value": 1, + "unit": "fraction of a lap, applied per wrap", + "source": "src/Kaneshige/KartChecker.cpp:461" + }, + { + "name": "progressFloor", + "value": -100, + "unit": "laps of accumulated progress", + "source": "src/Kaneshige/KartChecker.cpp:471" + }, + { + "name": "velocityToTimeScale", + "value": 0.06, + "unit": "world units per update converted to per frame distance, applied once per lap completion", + "source": "src/Kaneshige/KartChecker.cpp:610" + }, + { + "name": "reverseDotThreshold", + "value": -0.5, + "unit": "unitless dot product, sampled every update", + "source": "src/Kaneshige/KartChecker.cpp:771" + }, + { + "name": "fractionalClamp", + "value": -0.001, + "unit": "fraction of a sector, applied per query", + "source": "src/Kaneshige/Course/Course.cpp:1795" + } + ], + "sources": [ + { + "path": "src/Kaneshige/KartChecker.cpp", + "line": 387 + }, + { + "path": "src/Kaneshige/KartChecker.cpp", + "line": 457 + }, + { + "path": "src/Kaneshige/KartChecker.cpp", + "line": 758 + }, + { + "path": "src/Kaneshige/KartChecker.cpp", + "line": 603 + }, + { + "path": "src/Kaneshige/Course/Course.cpp", + "line": 1779 + } + ], + "capabilities": [ + "checkpoint plane progress", + "lap wrap detection", + "shortcut rejection", + "reverse detection" + ], + "tags": [ + "race", + "lap", + "tracking", + "progress" + ] + }, + { + "id": "kart-ranking", + "node": "race-flow", + "name": "Race ranking pass", + "summary": "Ranks are assigned from the outside in: finished karts by time from the top, running karts by remaining distance from the bottom", + "behavior": "The ranking pass runs a two ended sweep. On each iteration it scans every still unranked kart and keeps two candidates: the finished kart with the earliest total time, or where no time is available the one that crossed the goal on the earliest frame, and the running kart with the smallest remaining distance to its next checkpoint. The distance comparison starts from a large constant so a candidate is always found while at least one kart remains. The finished candidate takes the next rank from the top and the running candidate takes the next rank from the bottom, and the loop walks inward until the two ends meet. That is why a kart finishing while others still race slots in ahead of everyone, and why the rest of the field is ordered purely by track progress. Battle modes run their own passes that rank on balloons, robberies or bomb points instead of distance.", + "constants": [ + { + "name": "distanceSearchInitialBound", + "value": 10000, + "unit": "world units of remaining track distance, initialised each rank pass", + "source": "src/Kaneshige/RaceMgr.cpp:1051" + } + ], + "sources": [ + { + "path": "src/Kaneshige/RaceMgr.cpp", + "line": 1043 + }, + { + "path": "src/Kaneshige/RaceMgr.cpp", + "line": 1051 + }, + { + "path": "src/Kaneshige/RaceMgr.cpp", + "line": 1094 + }, + { + "path": "src/Kaneshige/RaceMgr.cpp", + "line": 792 + } + ], + "capabilities": [ + "two ended ranking", + "finish order by time", + "progress order by distance", + "battle mode ranking" + ], + "tags": [ + "race", + "ranking", + "progress", + "rules" + ] + }, + { + "id": "item-box-economy", + "node": "item-system", + "name": "Item box recycling and world item budget", + "summary": "Boxes stay inactive for a fixed time after being taken, and the number of items alive at once is capped per kind and per mode", + "behavior": "A box that is hit switches to an inactive state and counts up; while the count is under the configured rest time it stays invisible and cannot be taken, and once it passes the box opens again and returns to the pool. The rest time is a single stored value that a mode may override before play starts, so a battle mode can cycle boxes faster than a race. Separately the manager keeps per kind caps on how many of an item may exist and how many may move at once, plus a running budget on the total number of moving items in the world, evicting by priority whenever a new one would exceed it. Bomb battle raises the per kind and total caps far above their race values, because a battle run with the ordinary caps would starve. Item selection is handled by a shuffle object per kart and per driver, and karts whose driver is one of the two special ones are handed a different shuffle implementation reserved for the special racers.", + "constants": [ + { + "name": "boxRestFrames", + "value": 60, + "unit": "frames at 60 Hz", + "source": "src/Sato/GeoItemBox.cpp:22" + }, + { + "name": "movingItemBudget", + "value": 16, + "unit": "items alive at once, checked every spawn", + "source": "src/Sato/ItemObjMgr.cpp:59" + }, + { + "name": "battleMovingItemCap", + "value": 64, + "unit": "items alive at once in bomb battle, checked every spawn", + "source": "src/Sato/ItemObjMgr.cpp:169" + }, + { + "name": "battleItemKindCap", + "value": 64, + "unit": "items of one kind in bomb battle, checked every spawn", + "source": "src/Sato/ItemObjMgr.cpp:169" + }, + { + "name": "heartContactRadius", + "value": 150, + "unit": "world units, used per contact test", + "source": "src/Sato/ItemObjMgr.cpp:62" + }, + { + "name": "heldBombCap", + "value": 5, + "unit": "items held, checked every pickup", + "source": "src/Sato/ItemObjMgr.cpp:63" + }, + { + "name": "enemyShellSlotWeight", + "value": 1.5, + "unit": "weight multiplier per roulette roll", + "source": "src/Sato/ItemObjMgr.cpp:64" + }, + { + "name": "thunderLockoutSeconds", + "value": 30, + "unit": "seconds at 60 Hz, stored as a frame product, reset per race", + "source": "src/Sato/ItemObjMgr.cpp:65" + } + ], + "sources": [ + { + "path": "src/Sato/GeoItemBox.cpp", + "line": 22 + }, + { + "path": "src/Sato/GeoItemBox.cpp", + "line": 302 + }, + { + "path": "src/Sato/ItemObjMgr.cpp", + "line": 159 + }, + { + "path": "src/Sato/ItemObjMgr.cpp", + "line": 176 + }, + { + "path": "src/Sato/ItemObjMgr.cpp", + "line": 1195 + } + ], + "capabilities": [ + "box rest timer", + "per kind caps", + "global item budget", + "special driver weighting" + ], + "tags": [ + "items", + "spawning", + "economy", + "modes" + ] + }, + { + "id": "shell-projectile", + "node": "item-projectiles", + "name": "Rolling shell flight", + "summary": "A thrown shell leaves the hand with the thrower's velocity added, falls under its own gravity, bounces along the ground and dies after a limited number of wall hits", + "behavior": "On release the shell's direction comes from the thrower's hand matrix, is scaled to a launch speed and then has the thrower's own velocity added, so a shell thrown from a fast kart travels faster. A horizontal stick contribution is folded in unless the throw is the backwards variant, which simply flips the horizontal components. While flying the shell adds its gravity to vertical velocity each update, integrates position, snaps itself to ground height plus a height offset when it would sink below it, and reflects the vertical velocity with a small loss while redirecting the horizontal velocity along the ground normal so it follows a banked surface. A cap on total speed keeps a shell from accelerating without limit down a long slope. Every wall contact increments a counter, and when that counter reaches its limit the shell is teleported back to where it entered the world and removed, which bounds its life without an explicit timer. Special variants get twice the wall allowance, and a freshly released shell is given a short no collision window so it can clear the kart that threw it.", + "constants": [ + { + "name": "shellGravity", + "value": -3.5, + "unit": "world units per update of vertical acceleration, applied every update", + "source": "src/Sato/ItemGTurtle.cpp:35" + }, + { + "name": "shellLaunchSpeed", + "value": 100, + "unit": "world units per update", + "source": "src/Sato/ItemGTurtle.cpp:36" + }, + { + "name": "specialLaunchSpeed", + "value": 20, + "unit": "world units per update", + "source": "src/Sato/ItemGTurtle.cpp:37" + }, + { + "name": "shellBounceRetention", + "value": 0.99, + "unit": "fraction of vertical speed kept per bounce", + "source": "src/Sato/ItemGTurtle.cpp:41" + }, + { + "name": "shellBounceGain", + "value": 1.05, + "unit": "fraction of vertical speed gained per bounce", + "source": "src/Sato/ItemGTurtle.cpp:42" + }, + { + "name": "shellNoCollisionFrames", + "value": 5, + "unit": "frames at 60 Hz per release", + "source": "src/Sato/ItemGTurtle.cpp:43" + }, + { + "name": "specialNoCollisionFrames", + "value": 10, + "unit": "frames at 60 Hz per release", + "source": "src/Sato/ItemGTurtle.cpp:44" + }, + { + "name": "handOffsetFrames", + "value": 20, + "unit": "frames at 60 Hz of travel along the launch offset", + "source": "src/Sato/ItemGTurtle.cpp:45" + }, + { + "name": "wallHitLimit", + "value": 10, + "unit": "wall contacts before removal", + "source": "src/Sato/ItemGTurtle.cpp:56" + }, + { + "name": "shellSpeedCap", + "value": 150, + "unit": "world units per update", + "source": "src/Sato/ItemGTurtle.cpp:58" + } + ], + "sources": [ + { + "path": "src/Sato/ItemGTurtle.cpp", + "line": 187 + }, + { + "path": "src/Sato/ItemGTurtle.cpp", + "line": 217 + }, + { + "path": "src/Sato/ItemGTurtle.cpp", + "line": 251 + }, + { + "path": "src/Sato/ItemGTurtle.cpp", + "line": 344 + }, + { + "path": "src/Sato/ItemGTurtle.cpp", + "line": 318 + } + ], + "capabilities": [ + "hand anchored launch", + "kart velocity inheritance", + "ground bounce", + "wall contact budget" + ], + "tags": [ + "items", + "projectile", + "physics", + "combat" + ] + }, + { + "id": "homing-shell", + "node": "item-projectiles", + "name": "Homing shell steering", + "summary": "The flying shell waits, follows a path while hunting, chases a target from an offset behind it, then commits to a straight final dive", + "behavior": "The homing shell runs a small state machine. It first rides along on its owner's velocity, losing vertical speed, until a short wait window ends. It then follows authored path points, steering towards the next point with a fixed added velocity and shedding speed each update. Once it has a target, the search state aims at the target's horizontal position, adds a steering velocity, matches the target's own speed when the target is close and slows instead while still far away. If a full search window passes without resolving, it flips into an attack state where it commits to a straight line towards the target, accelerates along it, caps its speed and expires after its own attack window. A small angle comparison decides whether the shell snaps to a new direction matrix or blends between old and new headings, so a shell already pointing the right way does not visibly wobble.", + "constants": [ + { + "name": "searchOffsetDistance", + "value": 350, + "unit": "world units behind the target", + "source": "src/Sato/ItemFlyTurtle.cpp:38" + }, + { + "name": "searchSteerSpeed", + "value": 30, + "unit": "world units per update, added every search update", + "source": "src/Sato/ItemFlyTurtle.cpp:20" + }, + { + "name": "searchNearDistance", + "value": 250, + "unit": "world units, checked every search update", + "source": "src/Sato/ItemFlyTurtle.cpp:39" + }, + { + "name": "searchSlowdown", + "value": 0.85, + "unit": "fraction of speed kept per search update", + "source": "src/Sato/ItemFlyTurtle.cpp:40" + }, + { + "name": "maxSearchFrames", + "value": 30, + "unit": "frames at 60 Hz", + "source": "src/Sato/ItemFlyTurtle.cpp:33" + }, + { + "name": "attackSteerSpeed", + "value": 20, + "unit": "world units per update, added every attack update", + "source": "src/Sato/ItemFlyTurtle.cpp:34" + }, + { + "name": "commitDistance", + "value": 200, + "unit": "world units, checked every attack update", + "source": "src/Sato/ItemFlyTurtle.cpp:35" + }, + { + "name": "attackSpeedCap", + "value": 200, + "unit": "world units per update", + "source": "src/Sato/ItemFlyTurtle.cpp:36" + }, + { + "name": "maxAttackFrames", + "value": 120, + "unit": "frames at 60 Hz", + "source": "src/Sato/ItemFlyTurtle.cpp:44" + }, + { + "name": "orientationSnapAngle", + "value": 2, + "unit": "degrees, checked every update", + "source": "src/Sato/ItemFlyTurtle.cpp:31" + }, + { + "name": "radiansToDegrees", + "value": 57.29578, + "unit": "degrees per radian, applied every update", + "source": "src/Sato/ItemFlyTurtle.cpp:448" + } + ], + "sources": [ + { + "path": "src/Sato/ItemFlyTurtle.cpp", + "line": 361 + }, + { + "path": "src/Sato/ItemFlyTurtle.cpp", + "line": 409 + }, + { + "path": "src/Sato/ItemFlyTurtle.cpp", + "line": 445 + }, + { + "path": "src/Sato/ItemFlyTurtle.cpp", + "line": 475 + } + ], + "capabilities": [ + "target hunting", + "offset pursuit", + "speed matching", + "straight dive", + "heading blending" + ], + "tags": [ + "items", + "projectile", + "steering", + "ai" + ] + }, + { + "id": "traffic-car-drive", + "node": "traffic-cars", + "name": "Ambient traffic behaviour", + "summary": "Road traffic accelerates towards a target point, damps itself, and swaps into damage and recovery states when something hits it", + "behavior": "Each traffic car owns a target position derived from an authored path point. Every update it steers towards that target with a handle power, adds an acceleration value while below its intended speed, and applies an air resistance fraction everywhere above it, which gives the cars their glide and their soft speed ceiling. Gravity applies separately while a car is off the ground, with a lighter value used in the damaged state so a knocked car floats longer than it should before settling. A horn check compares distance and facing between a kart and the car, so a warning only plays from vehicles actually ahead of the player and pointing the same way. Collisions are resolved against karts and against items, and a hit redirects the car into a damage state whose velocity is seeded from the impact; from there it runs through recover, death and wait states before returning to path following.", + "constants": [ + { + "name": "trafficGravity", + "value": 10, + "unit": "world units per update of vertical acceleration", + "source": "src/Kaneshige/GeoCar.cpp:26" + }, + { + "name": "trafficAirResistance", + "value": 0.95, + "unit": "fraction of speed kept per update", + "source": "src/Kaneshige/GeoCar.cpp:28" + }, + { + "name": "trafficAcceleration", + "value": 3, + "unit": "world units per update, added while below target speed", + "source": "src/Kaneshige/GeoCar.cpp:25" + }, + { + "name": "trafficHandlePower", + "value": 3, + "unit": "steer response units per update", + "source": "src/Kaneshige/GeoCar.cpp:29" + }, + { + "name": "hornDistance", + "value": 1200, + "unit": "world units, checked every update", + "source": "src/Kaneshige/GeoCar.cpp:23" + }, + { + "name": "hornFacingDot", + "value": 0.5, + "unit": "unitless dot product, checked every update", + "source": "src/Kaneshige/GeoCar.cpp:24" + }, + { + "name": "damageVelocitySeed", + "value": 60, + "unit": "world units per update, set once per hit", + "source": "src/Kaneshige/GeoCar.cpp:33" + } + ], + "sources": [ + { + "path": "src/Kaneshige/GeoCar.cpp", + "line": 362 + }, + { + "path": "src/Kaneshige/GeoCar.cpp", + "line": 405 + }, + { + "path": "src/Kaneshige/GeoCar.cpp", + "line": 326 + }, + { + "path": "src/Kaneshige/GeoCar.cpp", + "line": 23 + } + ], + "capabilities": [ + "path point steering", + "soft speed ceiling", + "horn proximity test", + "damage and recover states" + ], + "tags": [ + "obstacle", + "movement", + "hazard", + "world" + ] + }, + { + "id": "kart-attribute-tables", + "node": "kart-roster", + "name": "Driver and vehicle attributes", + "summary": "Driver records carry a weight class and a default partner, vehicle records carry a weight class and a wheel count, and both are resolved through lookup switches", + "behavior": "Two small record types hold everything the rest of the code asks about a driver or a vehicle. A driver record pairs an identity with a partner identity, a weight class, a default vehicle and two small flags; a vehicle record pairs an identity with a weight class, a wheel count, a small offset and the driver it belongs to. Lookups are switches over the identity enumerations returning pointers into static tables, so a query costs a branch and never an allocation. The weight class is exposed as a plain integer for drivers and as an enumeration for vehicles, and the wheel count is the only place where the six-wheeled bodies differ structurally from the four-wheeled ones. Anything that needs a kart's physical character reads these tables indirectly, which is why a pairing that puts a light driver in a heavy vehicle ends up handling like the vehicle rather than the driver.", + "constants": [ + { + "name": "lightDriverWeightClass", + "value": 0, + "unit": "class index", + "source": "src/Kaneshige/KartInfo.cpp:12" + }, + { + "name": "mediumDriverWeightClass", + "value": 1, + "unit": "class index", + "source": "src/Kaneshige/KartInfo.cpp:44" + }, + { + "name": "heavyDriverWeightClass", + "value": 2, + "unit": "class index", + "source": "src/Kaneshige/KartInfo.cpp:164" + }, + { + "name": "standardWheelCount", + "value": 4, + "unit": "wheels per vehicle", + "source": "src/Kaneshige/KartInfo.cpp:173" + }, + { + "name": "heavyVehicleWheelCount", + "value": 6, + "unit": "wheels per vehicle", + "source": "src/Kaneshige/KartInfo.cpp:229" + } + ], + "sources": [ + { + "path": "src/Kaneshige/KartInfo.cpp", + "line": 363 + }, + { + "path": "src/Kaneshige/KartInfo.cpp", + "line": 431 + }, + { + "path": "src/Kaneshige/KartInfo.cpp", + "line": 502 + }, + { + "path": "src/Kaneshige/KartInfo.cpp", + "line": 514 + } + ], + "capabilities": [ + "weight class lookup", + "wheel count", + "default pairings", + "constant time table access" + ], + "tags": [ + "roster", + "attributes", + "handling", + "data" + ] + }, + { + "id": "terrain-collision-query", + "node": "course-terrain", + "name": "Ground and wall query", + "summary": "Collision is found by walking a uniform grid of candidate polygons around the kart and taking the deepest sphere intersection, with grazing depth rounding so contacts do not chatter", + "behavior": "A query starts from the kart's current and previous positions plus a radius that together describe a moving sphere. Dynamic objects are tested first so a moving platform or a crate can take priority over the static mesh. For the static mesh the query turns the sphere footprint into a grid aligned rectangle, converts that rectangle into grid indices through the course's own world to grid mapping, rejects indices outside the grid with a tiny tolerance, and then walks the candidate cells. Each candidate polygon is tested for a sphere intersection and the best depth is kept along with its normal, attribute and material. Finally the depth is rounded: when the contact normal and the incoming movement direction are nearly parallel the depth is replaced by a small constant instead of the computed value, which is what stops a kart sliding along a wall from being reported as deeply penetrating and bounced over and over.", + "constants": [ + { + "name": "defaultGroundHeight", + "value": 1000, + "unit": "world units, used when no collision is available", + "source": "src/Kaneshige/Course/CrsGround.cpp:48" + }, + { + "name": "planeOffsetScale", + "value": 0.0001, + "unit": "world units, applied per query", + "source": "src/Kaneshige/Course/CrsGround.cpp:164" + }, + { + "name": "gridIndexTolerance", + "value": 0.001, + "unit": "grid cells, applied per query", + "source": "src/Kaneshige/Course/CrsGround.cpp:686" + }, + { + "name": "gridSubdivision", + "value": 0.5, + "unit": "fraction of a cell, applied when splitting", + "source": "src/Kaneshige/Course/CrsGround.cpp:416" + }, + { + "name": "movementEpsilon", + "value": 0.1, + "unit": "squared world units, compared per query", + "source": "src/Kaneshige/Course/CrsGround.cpp:360" + }, + { + "name": "grazingNormalDot", + "value": -0.2, + "unit": "unitless dot product, checked per query", + "source": "src/Kaneshige/Course/CrsGround.cpp:395" + }, + { + "name": "grazingDepthFloor", + "value": 0.1, + "unit": "world units, substituted per query", + "source": "src/Kaneshige/Course/CrsGround.cpp:396" + } + ], + "sources": [ + { + "path": "src/Kaneshige/Course/CrsGround.cpp", + "line": 197 + }, + { + "path": "src/Kaneshige/Course/CrsGround.cpp", + "line": 390 + }, + { + "path": "src/Kaneshige/Course/CrsGround.cpp", + "line": 650 + }, + { + "path": "src/Kaneshige/Course/CrsGround.cpp", + "line": 671 + }, + { + "path": "src/Kaneshige/Course/CrsGround.cpp", + "line": 399 + } + ], + "capabilities": [ + "grid accelerated polygon search", + "sphere versus polygon depth", + "grazing depth rounding", + "surface attribute reporting" + ], + "tags": [ + "collision", + "terrain", + "query", + "spatial" + ] + } + ] +} diff --git a/data/games/nascar-98.json b/data/games/nascar-98.json new file mode 100644 index 0000000..e720def --- /dev/null +++ b/data/games/nascar-98.json @@ -0,0 +1,1379 @@ +{ + "version": 1, + "id": "nascar-98", + "title": "NASCAR 98", + "repo": "https://github.com/shohamc1/nascar98", + "sha": "95edb38924dcdbd12d659f635ba483c51d42e268", + "nodes": [ + { + "id": "car-handling", + "name": "Player car handling", + "purpose": "Per-frame steering input, speed-scaled steering lock, lateral slide build-up and recovery, cornering scrub and surface grip for the human-driven stock car", + "paths": [ + "src/objects.c" + ], + "functions": [ + { + "name": "CarApplyCorneringDrag", + "path": "src/objects.c", + "lines": [ + 5696, + 5771 + ] + }, + { + "name": "CarUpdatePlayer", + "path": "src/objects.c", + "lines": [ + 6285, + 6568 + ] + }, + { + "name": "func_8002A954", + "path": "src/objects.c", + "lines": [ + 6585, + 6601 + ] + }, + { + "name": "CarGetSteeringDivisor", + "path": "src/objects.c", + "lines": [ + 6603, + 6612 + ] + }, + { + "name": "CarUpdateYawForce", + "path": "src/objects.c", + "lines": [ + 6621, + 6689 + ] + }, + { + "name": "CarGetWheelGrip", + "path": "src/objects.c", + "lines": [ + 6911, + 6998 + ] + } + ], + "dependsOn": [] + }, + { + "id": "drivetrain", + "name": "Engine, gearbox and draft", + "purpose": "Gear bands that map speed to engine rpm, manual and automatic shifting, the per-frame drive force with off-band penalties, coasting loss and the slipstream speed bonus behind another car", + "paths": [ + "src/objects.c" + ], + "functions": [ + { + "name": "CarUpdateEngineRpm", + "path": "src/objects.c", + "lines": [ + 5429, + 5547 + ] + }, + { + "name": "func_80028D80", + "path": "src/objects.c", + "lines": [ + 5549, + 5621 + ] + }, + { + "name": "func_80028FCC", + "path": "src/objects.c", + "lines": [ + 5633, + 5654 + ] + }, + { + "name": "CarShiftUp", + "path": "src/objects.c", + "lines": [ + 5657, + 5667 + ] + }, + { + "name": "CarShiftDown", + "path": "src/objects.c", + "lines": [ + 5669, + 5684 + ] + }, + { + "name": "CarTestDraft", + "path": "src/objects.c", + "lines": [ + 8016, + 8060 + ] + } + ], + "dependsOn": [ + "car-handling" + ] + }, + { + "id": "collision", + "name": "Car and wall collision", + "purpose": "Car-to-car contact with speed exchange, side impulses and the crash threshold, plus wall contact with scrape loss, rebound impulse and spark effects", + "paths": [ + "src/objects.c" + ], + "functions": [ + { + "name": "CarHandlePairCollision", + "path": "src/objects.c", + "lines": [ + 3752, + 4432 + ] + }, + { + "name": "CarHandleWallCollision", + "path": "src/objects.c", + "lines": [ + 4462, + 5307 + ] + } + ], + "dependsOn": [ + "car-handling" + ] + }, + { + "id": "race-control", + "name": "Race control", + "purpose": "Lap counting at the start-finish line, fuel use, running order, finishing penalties, caution periods and the scripted crashes that bring out the yellow flag", + "paths": [ + "src/objects.c", + "src/scene.c" + ], + "functions": [ + { + "name": "RaceUpdateLapProgress", + "path": "src/objects.c", + "lines": [ + 18993, + 19181 + ] + }, + { + "name": "CarIsAheadOnSection", + "path": "src/objects.c", + "lines": [ + 19261, + 19291 + ] + }, + { + "name": "RaceUpdatePositions", + "path": "src/objects.c", + "lines": [ + 19298, + 19420 + ] + }, + { + "name": "RaceUpdateCrashTrigger", + "path": "src/scene.c", + "lines": [ + 2501, + 2573 + ] + }, + { + "name": "RaceInitCrashCandidates", + "path": "src/scene.c", + "lines": [ + 2581, + 2634 + ] + }, + { + "name": "RaceUpdateCaution", + "path": "src/scene.c", + "lines": [ + 7464, + 7582 + ] + } + ], + "dependsOn": [ + "collision" + ] + }, + { + "id": "opponent-ai", + "name": "Opponent field driver", + "purpose": "Computer cars that decide when a pass is possible, pick an avoidance point beside slower cars, and match speed when boxed in", + "paths": [ + "src/objects.c" + ], + "functions": [ + { + "name": "CarUpdateAi", + "path": "src/objects.c", + "lines": [ + 17778, + 17821 + ] + }, + { + "name": "CarCanPassOther", + "path": "src/objects.c", + "lines": [ + 17823, + 17836 + ] + }, + { + "name": "func_80039274", + "path": "src/objects.c", + "lines": [ + 17912, + 17968 + ] + }, + { + "name": "CarAvoidTraffic", + "path": "src/objects.c", + "lines": [ + 18142, + 18307 + ] + } + ], + "dependsOn": [ + "race-control", + "drivetrain" + ] + } + ], + "cards": [ + { + "id": "digital-steering-ramp", + "node": "car-handling", + "name": "Digital steering ramp", + "summary": "With a digital pad the wheel angle grows by an accelerating step each frame and returns to center when released", + "behavior": "While a direction is held, the per-frame steering step starts at 4 and grows by 3 each frame up to 12. The wheel angle gains 4 plus the step each frame and stops at the current steering lock. The opposite step resets to zero, so a quick reversal starts slowly again. With no direction held, the angle returns toward center by 8 per frame and stops at zero. An analog stick instead sets the angle directly as stick deflection times the lock divided by 112. The heading input applied to the car is a quarter of the wheel angle. A quarter value of one in either direction counts as zero, which gives a small dead zone.", + "constants": [ + { + "name": "firstStep", + "value": 4, + "unit": "steering angle units per frame, rate unverified", + "source": "src/objects.c:6427" + }, + { + "name": "stepGrowth", + "value": 3, + "unit": "steering angle units per frame per frame, rate unverified", + "source": "src/objects.c:6429" + }, + { + "name": "maxStep", + "value": "0xC", + "unit": "steering angle units per frame, hexadecimal, rate unverified", + "source": "src/objects.c:6433" + }, + { + "name": "centerReturn", + "value": 8, + "unit": "steering angle units per frame, rate unverified", + "source": "src/objects.c:6480" + }, + { + "name": "analogDivisor", + "value": 112, + "unit": "divisor on stick deflection, static", + "source": "src/objects.c:6498" + }, + { + "name": "wheelToHeading", + "value": 4, + "unit": "divisor from wheel angle to heading input, static", + "source": "src/objects.c:6504" + } + ], + "sources": [ + { + "path": "src/objects.c", + "line": 6285 + }, + { + "path": "src/objects.c", + "line": 6424 + }, + { + "path": "src/objects.c", + "line": 6498 + } + ], + "capabilities": [ + "digital steering", + "analog steering", + "self-centering" + ], + "tags": [ + "steering", + "input", + "controls", + "racing" + ] + }, + { + "id": "speed-scaled-steering-lock", + "node": "car-handling", + "name": "Speed-scaled steering lock", + "summary": "Above a threshold speed the steering lock shrinks along a slope set by the car setup and its grip", + "behavior": "The base steering lock comes from the handling setup and is multiplied by 4. Two optional buttons scale it to three quarters or four thirds. Above speed 200, the lock is scaled by 1000 minus the speed over 200 times a slope divided by a grip divisor, in thousandths. The slope blends between two setup values by the steering setting, measured from 50 over a range of 50. The grip divisor is 3000 plus an orientation term weighted by 200, plus front grip times 35 and rear grip times 5. Higher grip therefore keeps more lock at speed. The lock never falls below 8.", + "constants": [ + { + "name": "falloffStartSpeed", + "value": 200, + "unit": "speed units, 16.16 fixed point integer part, scale unverified", + "source": "src/objects.c:6409" + }, + { + "name": "fullLockFactor", + "value": 1000, + "unit": "thousandths, static", + "source": "src/objects.c:6409" + }, + { + "name": "settingRange", + "value": 50, + "unit": "setting steps, static", + "source": "src/objects.c:6402" + }, + { + "name": "baseDivisor", + "value": 3000, + "unit": "divisor, static", + "source": "src/objects.c:6610" + }, + { + "name": "bodyWeight", + "value": 200, + "unit": "multiplier on body term, static", + "source": "src/objects.c:6608" + }, + { + "name": "frontGripWeight", + "value": 35, + "unit": "multiplier on front grip, static", + "source": "src/objects.c:6609" + }, + { + "name": "rearGripWeight", + "value": 5, + "unit": "multiplier on rear grip, static", + "source": "src/objects.c:6609" + }, + { + "name": "minimumLock", + "value": 8, + "unit": "steering angle units, static", + "source": "src/objects.c:6419" + } + ], + "sources": [ + { + "path": "src/objects.c", + "line": 6400 + }, + { + "path": "src/objects.c", + "line": 6603 + }, + { + "path": "src/objects.c", + "line": 6408 + } + ], + "capabilities": [ + "speed-sensitive steering", + "setup tuning" + ], + "tags": [ + "steering", + "handling", + "setup", + "racing" + ] + }, + { + "id": "lateral-slide", + "node": "car-handling", + "name": "Lateral slide build-up", + "summary": "Steering at speed builds a sideways slide value that rear grip bleeds off, faster when counter-steering", + "behavior": "Below speed 100 the slide value is cleared and nothing happens. Above it, each frame adds two fifths of the steering angle times speed, weighted by half of throttle plus one, plus a fixed bias toward the steer side. That step is capped at the rear grip recovery plus 500. Rear grip times 100, minus 500, is then removed from the slide each frame toward zero. Steering against the slide doubles that recovery. The yaw force returned is the slide value times 16, clamped once its magnitude passes 1000 bands. Some slide bands set a slide feedback flag when the steering step exceeds the recovery.", + "constants": [ + { + "name": "minSlideSpeed", + "value": 100, + "unit": "speed units, 16.16 fixed point integer part, scale unverified", + "source": "src/objects.c:6631" + }, + { + "name": "steerShareNumerator", + "value": 2, + "unit": "fraction numerator of steering angle, static", + "source": "src/objects.c:6638" + }, + { + "name": "steerShareDenominator", + "value": 5, + "unit": "fraction denominator of steering angle, static", + "source": "src/objects.c:6638" + }, + { + "name": "recoveryOffset", + "value": 500, + "unit": "slide units per frame, rate unverified", + "source": "src/objects.c:6635" + }, + { + "name": "rearGripRecoveryWeight", + "value": 100, + "unit": "multiplier on rear grip, static", + "source": "src/objects.c:6635" + }, + { + "name": "slideBandClamp", + "value": 1000, + "unit": "slide bands, static", + "source": "src/objects.c:6680" + } + ], + "sources": [ + { + "path": "src/objects.c", + "line": 6621 + }, + { + "path": "src/objects.c", + "line": 6656 + }, + { + "path": "src/objects.c", + "line": 6671 + } + ], + "capabilities": [ + "slide model", + "counter-steer recovery", + "slide feedback flag" + ], + "tags": [ + "handling", + "slide", + "physics", + "racing" + ] + }, + { + "id": "cornering-scrub", + "node": "car-handling", + "name": "Cornering scrub and speed loss", + "summary": "Cornering force grows with speed squared and tighter turns, and force past the tire hold scrubs speed and pushes the car outward", + "behavior": "Each frame, a cornering force equals speed squared times a car factor, divided by a term that shrinks as the turn input grows: 14 minus the turn, times 1000, plus 14000. The force doubles when it opposes the current slide. It joins the slide value together with an orientation term. A fixed decay pulls the slide toward zero, twice as hard when the car goes straight. Each of the four tires holds 2000 units of the slide. The excess, divided by 256 and then by 16, comes off forward speed, so overdriving a corner costs speed. The excess divided by 200 also pushes the car along its lateral axis.", + "constants": [ + { + "name": "turnTermScale", + "value": 1000, + "unit": "divisor scale per turn step, static", + "source": "src/objects.c:5720" + }, + { + "name": "turnTermBase", + "value": 14000, + "unit": "divisor base, static", + "source": "src/objects.c:5720" + }, + { + "name": "tireHold", + "value": 2000, + "unit": "slide units per tire, static", + "source": "src/objects.c:8008" + }, + { + "name": "scrubDivisor", + "value": 256, + "unit": "divisor on excess slide, then divided by 16, per frame, rate unverified", + "source": "src/objects.c:5758" + }, + { + "name": "lateralPushDivisor", + "value": 200, + "unit": "divisor on excess slide, per frame, rate unverified", + "source": "src/objects.c:5762" + } + ], + "sources": [ + { + "path": "src/objects.c", + "line": 5696 + }, + { + "path": "src/objects.c", + "line": 5724 + }, + { + "path": "src/objects.c", + "line": 5761 + } + ], + "capabilities": [ + "cornering drag", + "understeer push", + "speed scrub" + ], + "tags": [ + "handling", + "cornering", + "physics", + "racing" + ] + }, + { + "id": "surface-grip", + "node": "car-handling", + "name": "Off-track surface grip", + "summary": "Each wheel reads a grip value from its surface code, and rough surfaces bleed wheel velocity and throw up debris", + "behavior": "A wheel on normal track returns a grip of one half. On surface code 2 the front wheels return one third and the rear wheels one quarter. Above a mid speed that surface scales wheel velocity by 99 hundredths per frame when a global flag is set, and above a lower speed it spawns dirt at the wheel. At high speed it also starts a random wheel wobble capped at 14. On surface code 5, above a high speed, wheel velocity scales by 63 sixty-fourths per frame, with a front grip of one half and a rear grip of one quarter. Rear wheels always lose grip first, which makes the tail step out when a car drops a wheel off the track.", + "constants": [ + { + "name": "normalGripDenominator", + "value": 2, + "unit": "fraction denominator, static", + "source": "src/objects.c:6997" + }, + { + "name": "roughFrontGripDenominator", + "value": 3, + "unit": "fraction denominator, static", + "source": "src/objects.c:6994" + }, + { + "name": "rearGripDenominator", + "value": 4, + "unit": "fraction denominator, static", + "source": "src/objects.c:6929" + }, + { + "name": "roughVelocityScale", + "value": "0x63, 0x64", + "unit": "numerator and denominator in hexadecimal, per frame, rate unverified", + "source": "src/objects.c:6943" + }, + { + "name": "slickVelocityScale", + "value": "0x3F, 0x40", + "unit": "numerator and denominator in hexadecimal, per frame, rate unverified", + "source": "src/objects.c:6926" + }, + { + "name": "maxWobble", + "value": 14, + "unit": "wobble units, static", + "source": "src/objects.c:6964" + } + ], + "sources": [ + { + "path": "src/objects.c", + "line": 6911 + }, + { + "path": "src/objects.c", + "line": 6922 + }, + { + "path": "src/objects.c", + "line": 6937 + } + ], + "capabilities": [ + "surface grip", + "off-track penalty", + "dirt effect" + ], + "tags": [ + "tires", + "surface", + "grip", + "physics" + ] + }, + { + "id": "gear-band-rpm", + "node": "drivetrain", + "name": "Gear bands and engine rpm", + "summary": "Each gear covers a speed band, and rpm rises linearly across the band from its low to its high value", + "behavior": "A gear table gives each gear a start speed, an end speed, a low rpm and a high rpm. With the automatic gearbox, the gear is the band that contains the current speed, and rpm interpolates linearly inside it. The top gear runs up to the car maximum speed. After an upshift, automatic selection waits 20 frames. With the manual gearbox the gear stays put: below its band rpm scales down from the low value, and above it rpm holds at the high value. In most game modes, a manual car pushed past its band also gets a forward nudge of 5 speed units. Reverse holds rpm at 10. A manual downshift drops rpm to the new gear low value at once.", + "constants": [ + { + "name": "upshiftHold", + "value": "0x14", + "unit": "frames, hexadecimal, rate unverified", + "source": "src/objects.c:5476" + }, + { + "name": "overBandNudge", + "value": "0x50000", + "unit": "speed in 16.16 fixed point, hexadecimal, scale unverified", + "source": "src/objects.c:5540" + }, + { + "name": "reverseRpm", + "value": 10, + "unit": "rpm, static", + "source": "src/objects.c:5441" + } + ], + "sources": [ + { + "path": "src/objects.c", + "line": 5429 + }, + { + "path": "src/objects.c", + "line": 5460 + }, + { + "path": "src/objects.c", + "line": 5682 + } + ], + "capabilities": [ + "gearbox", + "rpm model", + "automatic shifting" + ], + "tags": [ + "engine", + "gearbox", + "audio feel", + "racing" + ] + }, + { + "id": "drive-force", + "node": "drivetrain", + "name": "Drive force and coasting", + "summary": "Throttle adds a gear-ratio force each frame, reduced outside the gear band, and lifting off costs a fixed speed per frame", + "behavior": "With throttle held, each frame adds the gear ratio to speed until the target speed. With a manual gearbox, a car below its band gets the ratio scaled by speed plus 80 over the band start. A car above its band gets the ratio scaled by 16 over the excess plus 16. A car inside its band gets a bonus of one sixth. The automatic gearbox only applies the over-band falloff in top gear. An optional assist multiplies a human car force by its rank among the human players, so the trailing player in split screen gets twice the force. The force is then multiplied by seven quarters, and one sixteenth of the draft bonus is added. Without throttle, speed falls by 4 units per frame toward the target. A car out of fuel or with a flagged engine gets no force.", + "constants": [ + { + "name": "inBandBonusDenominator", + "value": 6, + "unit": "fraction denominator, per frame, rate unverified", + "source": "src/objects.c:5598" + }, + { + "name": "overBandFalloff", + "value": "0x10", + "unit": "speed units, hexadecimal, static", + "source": "src/objects.c:5596" + }, + { + "name": "finalScaleNumerator", + "value": 8, + "unit": "multiplier before subtracting one share and dividing by 4, static", + "source": "src/objects.c:5616" + }, + { + "name": "draftShareShift", + "value": 4, + "unit": "bit shift on draft bonus, divides by 16, static", + "source": "src/objects.c:5619" + }, + { + "name": "coastLoss", + "value": "0x40000", + "unit": "speed in 16.16 fixed point, hexadecimal, scale unverified, per frame, rate unverified", + "source": "src/objects.c:5647" + } + ], + "sources": [ + { + "path": "src/objects.c", + "line": 5549 + }, + { + "path": "src/objects.c", + "line": 5620 + }, + { + "path": "src/objects.c", + "line": 5633 + } + ], + "capabilities": [ + "throttle model", + "gear penalty", + "coast down" + ], + "tags": [ + "engine", + "acceleration", + "drivetrain", + "racing" + ] + }, + { + "id": "draft-boost", + "node": "drivetrain", + "name": "Slipstream draft", + "summary": "A car directly behind another inside a range gains a speed bonus that grows as the gap closes", + "behavior": "Draft range comes from a setup value: the value minus 50, times 80. For each car pair, the test first rejects any axis gap larger than that range. It then rotates the gap into the trailing car frame. The leading car must sit ahead, inside the range, and within twice a body extent of the trailing car center line. The bonus is the range minus the forward gap, divided by a strength value of 15000 over the setup value minus 70. One sixteenth of the bonus feeds the drive force each frame, so bump-drafting close behind a leader pays the most.", + "constants": [ + { + "name": "rangeOffset", + "value": "0x32", + "unit": "setup steps, hexadecimal, static", + "source": "src/objects.c:6588" + }, + { + "name": "rangeScale", + "value": "0x50", + "unit": "world units per setup step, hexadecimal, static", + "source": "src/objects.c:6588" + }, + { + "name": "strengthNumerator", + "value": "0x3A98", + "unit": "divisor numerator, hexadecimal, static", + "source": "src/objects.c:6592" + }, + { + "name": "strengthOffset", + "value": "0x46", + "unit": "setup steps, hexadecimal, static", + "source": "src/objects.c:6592" + }, + { + "name": "widthMultiplier", + "value": 2, + "unit": "multiplier on body half-length, static", + "source": "src/objects.c:8049" + } + ], + "sources": [ + { + "path": "src/objects.c", + "line": 8016 + }, + { + "path": "src/objects.c", + "line": 8052 + }, + { + "path": "src/objects.c", + "line": 6585 + } + ], + "capabilities": [ + "slipstream", + "drafting", + "pack racing" + ], + "tags": [ + "draft", + "aerodynamics", + "racing", + "oval" + ] + }, + { + "id": "bump-speed-exchange", + "node": "collision", + "name": "Bump contact speed exchange", + "summary": "Overlapping cars are pushed apart and both speeds are set near their average, one car gaining and the other losing a little", + "behavior": "Contact uses a box from each car model: half-width minus 10 and a length allowance shorter by 30 for two computer cars or by 15 otherwise. When the cars overlap below the crash speed, both are moved apart along the lateral axis of the first car. Their speeds become the mean of the two speeds, plus 10 for one car and minus 10 for the other, picked by the contact direction. Another contact branch adds an impulse, clamped to 50 on each axis, to the wheel velocities. A human car that side-swipes a computer car slows the computer car by 5.", + "constants": [ + { + "name": "widthInset", + "value": 10, + "unit": "world units, static", + "source": "src/objects.c:3877" + }, + { + "name": "aiLengthInset", + "value": "0x1E", + "unit": "world units, hexadecimal, static", + "source": "src/objects.c:3960" + }, + { + "name": "lengthInset", + "value": "0xF", + "unit": "world units, hexadecimal, static", + "source": "src/objects.c:3964" + }, + { + "name": "bumpSpeedOffset", + "value": "0xA0000", + "unit": "speed in 16.16 fixed point, hexadecimal, scale unverified", + "source": "src/objects.c:4010" + }, + { + "name": "sideImpulseClamp", + "value": "0x32", + "unit": "impulse units, hexadecimal, per contact", + "source": "src/objects.c:4248" + }, + { + "name": "sideSwipeSlow", + "value": "0xFFFB0000", + "unit": "speed in 16.16 fixed point, hexadecimal, scale unverified, negative five", + "source": "src/objects.c:4289" + } + ], + "sources": [ + { + "path": "src/objects.c", + "line": 3752 + }, + { + "path": "src/objects.c", + "line": 4006 + }, + { + "path": "src/objects.c", + "line": 4243 + } + ], + "capabilities": [ + "contact response", + "speed averaging", + "push apart" + ], + "tags": [ + "collision", + "contact", + "physics", + "racing" + ] + }, + { + "id": "contact-crash-threshold", + "node": "collision", + "name": "Contact crash threshold and sparks", + "summary": "High closing speed turns a contact into a crash state, and medium hits throw sparks with a random extra effect", + "behavior": "The closing speed of a pair is the length of their relative velocity. At 301 or more, the contact sets a crash response and switches each eligible car to the crash routine, so the car spins out of control. Below that, closing speeds of 46 or more spawn a contact effect, and 121 or more add a heavier effect. Each tier also rolls a one-in-ten chance of a second effect. When cars overlap deeply at low speed, both cars are placed 800 units from their midpoint along the line between them, which prevents the cars from stacking inside each other.", + "constants": [ + { + "name": "crashClosingSpeed", + "value": "0x12D", + "unit": "velocity units, hexadecimal, scale unverified", + "source": "src/objects.c:3976" + }, + { + "name": "lightEffectSpeed", + "value": "0x2E", + "unit": "velocity units, hexadecimal, scale unverified", + "source": "src/objects.c:4038" + }, + { + "name": "heavyEffectSpeed", + "value": "0x79", + "unit": "velocity units, hexadecimal, scale unverified", + "source": "src/objects.c:4039" + }, + { + "name": "extraEffectOdds", + "value": "0xA", + "unit": "one in this many, hexadecimal, per contact", + "source": "src/objects.c:4041" + }, + { + "name": "separationRadius", + "value": "0x320", + "unit": "world units, hexadecimal, static", + "source": "src/objects.c:4356" + } + ], + "sources": [ + { + "path": "src/objects.c", + "line": 4324 + }, + { + "path": "src/objects.c", + "line": 4040 + }, + { + "path": "src/objects.c", + "line": 3948 + } + ], + "capabilities": [ + "crash trigger", + "impact effects", + "overlap resolution" + ], + "tags": [ + "collision", + "crash", + "effects", + "racing" + ] + }, + { + "id": "wall-scrape", + "node": "collision", + "name": "Wall scrape and rebound", + "summary": "A glancing wall hit scrubs speed by the contact angle, and a firm hit bounces the car off with effects and can wreck it", + "behavior": "Headings use 1024 units per turn. When the car meets the wall at less than 140 units of angle, steering is zeroed and speed drops by twice the angle plus one sixteenth of speed. Each touching corner then gets a rebound impulse of three quarters of the velocity into the wall, applied along the wall normal. Impulses of 51 or more call a contact routine, 101 or more spawn an effect at the car, and 176 or more roll a chance for a second effect that rises with impulse. In one contact branch, a car with speed 371 or more switches to the crash routine.", + "constants": [ + { + "name": "glancingAngle", + "value": "0x8C", + "unit": "heading units of 1024 per turn, hexadecimal, static", + "source": "src/objects.c:4692" + }, + { + "name": "speedShareShift", + "value": 4, + "unit": "bit shift, divides speed by 16, per contact", + "source": "src/objects.c:4696" + }, + { + "name": "scrapeMarkImpulse", + "value": "0x33", + "unit": "impulse units, hexadecimal, per contact", + "source": "src/objects.c:4806" + }, + { + "name": "sparkImpulse", + "value": "0x65", + "unit": "impulse units, hexadecimal, per contact", + "source": "src/objects.c:4803" + }, + { + "name": "debrisImpulse", + "value": "0xB0", + "unit": "impulse units, hexadecimal, per contact", + "source": "src/objects.c:4798" + }, + { + "name": "wreckSpeed", + "value": "0x173", + "unit": "speed units, 16.16 fixed point integer part, scale unverified, hexadecimal", + "source": "src/objects.c:5162" + } + ], + "sources": [ + { + "path": "src/objects.c", + "line": 4462 + }, + { + "path": "src/objects.c", + "line": 4694 + }, + { + "path": "src/objects.c", + "line": 4793 + } + ], + "capabilities": [ + "wall collision", + "scrape loss", + "crash trigger" + ], + "tags": [ + "collision", + "walls", + "physics", + "oval" + ] + }, + { + "id": "lap-counting-fuel", + "node": "race-control", + "name": "Lap counting and fuel use", + "summary": "A lap counts when a car moves from the last track sections into the first ones, and human cars burn fuel by rpm", + "behavior": "Each car remembers its last track section. A move from section 11 or later into a section below 5 adds a lap. A move from section 0 back into section 11 or later removes one, so a car cannot gain laps by reversing over the line. A car past the lap limit is marked as finished. Every 32 ticks of the race clock each human car burns fuel equal to rpm divided by a fuel rate, and an empty tank stops the drive force. Race progress for the whole field is lap times 1000 plus section. The spotter calls the laps every 5 laps below lap 50 and every 10 laps below lap 100.", + "constants": [ + { + "name": "lateSection", + "value": 11, + "unit": "track section index, static", + "source": "src/objects.c:19046" + }, + { + "name": "earlySection", + "value": 5, + "unit": "track section index, static", + "source": "src/objects.c:19053" + }, + { + "name": "fuelIntervalMask", + "value": 31, + "unit": "race clock mask, every 32 ticks, rate unverified", + "source": "src/objects.c:19022" + }, + { + "name": "lapProgressScale", + "value": 1000, + "unit": "progress units per lap, static", + "source": "src/objects.c:19147" + }, + { + "name": "shortRaceCallInterval", + "value": 5, + "unit": "laps, static", + "source": "src/objects.c:19105" + }, + { + "name": "longRaceCallInterval", + "value": 10, + "unit": "laps, static", + "source": "src/objects.c:19116" + } + ], + "sources": [ + { + "path": "src/objects.c", + "line": 18993 + }, + { + "path": "src/objects.c", + "line": 19066 + }, + { + "path": "src/objects.c", + "line": 19035 + } + ], + "capabilities": [ + "lap counting", + "anti-reverse check", + "fuel consumption" + ], + "tags": [ + "race", + "laps", + "fuel", + "oval" + ] + }, + { + "id": "running-order", + "node": "race-control", + "name": "Running order and finish order", + "summary": "Positions sort by lap and section every 20 updates, ties break by distance along the section, and finishers get a lock-in bonus", + "behavior": "Every update the car list is sorted by track section for nearby checks. Every 20 updates, or on demand, each car gets a race key of lap plus one, times 1000, plus section plus a finish bonus. Cars sort by that key. Equal keys break by projecting both cars onto the section heading and comparing forward distance. A car that has passed the lap limit, or any car once a race-end flag is set, gets a permanent bonus of 30 minus its place, times 100000, so finish order cannot change. In time trials the player place comes from comparing the lap time against 23 stored times.", + "constants": [ + { + "name": "reorderInterval", + "value": 20, + "unit": "position updates, rate unverified", + "source": "src/objects.c:19338" + }, + { + "name": "lapKeyScale", + "value": 1000, + "unit": "race key units per lap, static", + "source": "src/objects.c:19348" + }, + { + "name": "finishBase", + "value": 30, + "unit": "places, static", + "source": "src/objects.c:19365" + }, + { + "name": "finishScale", + "value": 100000, + "unit": "race key units per place, static", + "source": "src/objects.c:19365" + }, + { + "name": "trialCompareCount", + "value": 23, + "unit": "stored times, static", + "source": "src/objects.c:19386" + } + ], + "sources": [ + { + "path": "src/objects.c", + "line": 19298 + }, + { + "path": "src/objects.c", + "line": 19261 + }, + { + "path": "src/objects.c", + "line": 19367 + } + ], + "capabilities": [ + "race positions", + "tie break", + "finish lock" + ], + "tags": [ + "race", + "positions", + "leaderboard", + "oval" + ] + }, + { + "id": "caution-period", + "node": "race-control", + "name": "Caution period and restart", + "summary": "A caution hands the player car to the computer driver for a fixed time, then counts down to a green-flag restart", + "behavior": "When a caution starts, the player car switches to the automatic gearbox and is driven by the same routine as the field. Fuel use is suspended, and a timer starts at 600 frames. The caution lasts until the timer runs out and every car has cleared its pending state flags. A car pointing more than 64 heading units off the track direction requests a field realignment step. The restart countdown then runs 100 frames. At 80 the flag display changes, below 50 the player regains control with the original gearbox, and at 40 normal racing resumes.", + "constants": [ + { + "name": "cautionFrames", + "value": 600, + "unit": "frames, rate unverified", + "source": "src/scene.c:7503" + }, + { + "name": "restartFrames", + "value": 100, + "unit": "frames, rate unverified", + "source": "src/scene.c:7543" + }, + { + "name": "greenFlagFrame", + "value": "0x50", + "unit": "frames remaining, hexadecimal, rate unverified", + "source": "src/scene.c:7548" + }, + { + "name": "controlReturnFrame", + "value": "0x32", + "unit": "frames remaining, hexadecimal, rate unverified", + "source": "src/scene.c:7561" + }, + { + "name": "racingResumeFrame", + "value": "0x28", + "unit": "frames remaining, hexadecimal, rate unverified", + "source": "src/scene.c:7553" + }, + { + "name": "offLineHeading", + "value": "0x40", + "unit": "heading units of 1024 per turn, hexadecimal, static", + "source": "src/scene.c:7529" + } + ], + "sources": [ + { + "path": "src/scene.c", + "line": 7464 + }, + { + "path": "src/scene.c", + "line": 7501 + }, + { + "path": "src/scene.c", + "line": 7536 + } + ], + "capabilities": [ + "caution flag", + "autopilot", + "race restart" + ], + "tags": [ + "race", + "caution", + "flags", + "oval" + ] + }, + { + "id": "scripted-crashes", + "node": "race-control", + "name": "Scripted field crashes", + "summary": "A few cars are picked at the start to wreck later, each after a random countdown, when running near full speed", + "behavior": "At race start, the crash count comes from two race settings, each divided by 50, plus a random term, and is scaled by the field size. Distinct cars are picked at random, and each gets a random countdown that scales with the lap count. Every 200 frames, when no crash is active and no caution is running, each candidate counts down by one. At zero, a computer car crashes once its speed passes nine tenths of a reference speed. A human candidate crashes once it is within 10 speed units of its own maximum. The crash is announced, and smoke spawns at the car center with a random offset of 100 units each way.", + "constants": [ + { + "name": "lengthDivisor", + "value": 50, + "unit": "race length units, static", + "source": "src/scene.c:2602" + }, + { + "name": "randomRange", + "value": 3, + "unit": "random range, static", + "source": "src/scene.c:2602" + }, + { + "name": "checkInterval", + "value": 200, + "unit": "frames, rate unverified", + "source": "src/scene.c:2509" + }, + { + "name": "speedShareNumerator", + "value": 9, + "unit": "tenths of a reference speed, static", + "source": "src/scene.c:2520" + }, + { + "name": "playerMargin", + "value": "0xA0000", + "unit": "speed in 16.16 fixed point, hexadecimal, scale unverified", + "source": "src/scene.c:2522" + }, + { + "name": "smokeJitter", + "value": 100, + "unit": "world units, static", + "source": "src/scene.c:2568" + } + ], + "sources": [ + { + "path": "src/scene.c", + "line": 2581 + }, + { + "path": "src/scene.c", + "line": 2501 + }, + { + "path": "src/scene.c", + "line": 2529 + } + ], + "capabilities": [ + "scripted events", + "random wrecks", + "crash announcement" + ], + "tags": [ + "race", + "crash", + "events", + "AI" + ] + }, + { + "id": "opponent-passing", + "node": "opponent-ai", + "name": "Opponent passing and traffic avoidance", + "summary": "A computer car looks a few sections ahead, steers to a point beside a slower car, or matches its speed when the lane is blocked", + "behavior": "A pass is possible when the car is faster and the speed difference divided by 20 covers the section gap, which must be below 2. Each frame the car checks others within the speed difference over 50 plus 1 sections, at most 5. It rotates each into its own frame. A car more than 200 units to the side, inside a side window of 400 plus a heading term up to 250, and with a forward offset between four times the closing speed minus 230 and 1350, triggers a lane check. If the lane is closed, the car takes the other speed plus or minus 40. Cars wider than the window are ignored. An overtaking target sits 1600 units ahead of the slower car and 350 units to one side. A car 1001 to 1349 units ahead makes this car match its speed minus 1 when that car is fast, or close the gap by a twentieth of the difference.", + "constants": [ + { + "name": "passGapDivisor", + "value": 20, + "unit": "speed difference per section of gap, static", + "source": "src/objects.c:17835" + }, + { + "name": "lookAheadDivisor", + "value": 50, + "unit": "speed units per section of look-ahead, static", + "source": "src/objects.c:18178" + }, + { + "name": "sideIgnore", + "value": 201, + "unit": "world units, static", + "source": "src/objects.c:18218" + }, + { + "name": "sideWindow", + "value": 400, + "unit": "world units, static", + "source": "src/objects.c:18217" + }, + { + "name": "headingWindow", + "value": 250, + "unit": "world units, static", + "source": "src/objects.c:18213" + }, + { + "name": "forwardWindow", + "value": 1350, + "unit": "world units, static", + "source": "src/objects.c:18228" + }, + { + "name": "rearWindow", + "value": 230, + "unit": "world units, static", + "source": "src/objects.c:18221" + }, + { + "name": "blockedSpeedOffset", + "value": "0x280000", + "unit": "speed in 16.16 fixed point, hexadecimal, scale unverified", + "source": "src/objects.c:18262" + }, + { + "name": "passPointAhead", + "value": 1600, + "unit": "world units, static", + "source": "src/objects.c:17940" + }, + { + "name": "passPointSide", + "value": 350, + "unit": "world units, static", + "source": "src/objects.c:17940" + } + ], + "sources": [ + { + "path": "src/objects.c", + "line": 17823 + }, + { + "path": "src/objects.c", + "line": 18142 + }, + { + "path": "src/objects.c", + "line": 17912 + }, + { + "path": "src/objects.c", + "line": 18292 + } + ], + "capabilities": [ + "overtaking", + "traffic avoidance", + "speed matching" + ], + "tags": [ + "AI", + "overtaking", + "racing", + "pack racing" + ] + } + ] +} diff --git a/data/games/need-for-speed-most-wanted.json b/data/games/need-for-speed-most-wanted.json new file mode 100644 index 0000000..635e21b --- /dev/null +++ b/data/games/need-for-speed-most-wanted.json @@ -0,0 +1,1682 @@ +{ + "version": 1, + "id": "need-for-speed-most-wanted", + "title": "Need for Speed: Most Wanted", + "repo": "https://github.com/dbalatoni13/nfsmw", + "sha": "80993f35224c61d2f1b135beba76c6f8234c7940", + "nodes": [ + { + "id": "racer-chassis", + "name": "Racer chassis and tire model", + "purpose": "Per-update handling for player and racer cars: tire lateral grip from load and slip angle, speed-sensitive steering, drift state, burnout fishtails, yaw assist, traction control and the slow-motion pursuit-breaker handling boost", + "paths": [ + "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp", + "src/Speed/Indep/Src/Physics/CarBasics.hpp" + ], + "functions": [ + { + "name": "SuspensionRacer::Tire::ComputeLateralForce", + "path": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp", + "lines": [ + 555, + 579 + ] + }, + { + "name": "SuspensionRacer::Tire::GetPilotFactor", + "path": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp", + "lines": [ + 582, + 598 + ] + }, + { + "name": "SuspensionRacer::OnTaskSimulate", + "path": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp", + "lines": [ + 940, + 1012 + ] + }, + { + "name": "SuspensionRacer::DoHumanSteering", + "path": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp", + "lines": [ + 1168, + 1215 + ] + }, + { + "name": "SuspensionRacer::CalculateMaxSteering", + "path": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp", + "lines": [ + 1221, + 1270 + ] + }, + { + "name": "SuspensionRacer::CalculateSteeringSpeed", + "path": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp", + "lines": [ + 1273, + 1288 + ] + }, + { + "name": "SuspensionRacer::Burnout::Update", + "path": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp", + "lines": [ + 1330, + 1363 + ] + }, + { + "name": "YawFrictionBoost", + "path": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp", + "lines": [ + 1414, + 1427 + ] + }, + { + "name": "SuspensionRacer::DoDrifting", + "path": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp", + "lines": [ + 1474, + 1561 + ] + }, + { + "name": "SuspensionRacer::TuneWheelParams", + "path": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp", + "lines": [ + 1572, + 1694 + ] + }, + { + "name": "SuspensionRacer::DoDriveForces", + "path": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp", + "lines": [ + 1724, + 1813 + ] + } + ], + "dependsOn": [] + }, + { + "id": "racer-engine", + "name": "Racer engine and gearbox", + "purpose": "Engine speed smoothing, automatic gear selection, shift timers, speed limiter and the nitrous tank that drains while engaged and refills with speed", + "paths": [ + "src/Speed/Indep/Src/Physics/Behaviors/EngineRacer.cpp" + ], + "functions": [ + { + "name": "EngineRacer::AutoShift", + "path": "src/Speed/Indep/Src/Physics/Behaviors/EngineRacer.cpp", + "lines": [ + 606, + 647 + ] + }, + { + "name": "EngineRacer::FindShiftPotential", + "path": "src/Speed/Indep/Src/Physics/Behaviors/EngineRacer.cpp", + "lines": [ + 653, + 680 + ] + }, + { + "name": "EngineRacer::OnGearChange", + "path": "src/Speed/Indep/Src/Physics/Behaviors/EngineRacer.cpp", + "lines": [ + 709, + 725 + ] + }, + { + "name": "Engine_SmoothRPM", + "path": "src/Speed/Indep/Src/Physics/Behaviors/EngineRacer.cpp", + "lines": [ + 888, + 904 + ] + }, + { + "name": "EngineRacer::DoECU", + "path": "src/Speed/Indep/Src/Physics/Behaviors/EngineRacer.cpp", + "lines": [ + 907, + 924 + ] + }, + { + "name": "EngineRacer::DoNos", + "path": "src/Speed/Indep/Src/Physics/Behaviors/EngineRacer.cpp", + "lines": [ + 931, + 993 + ] + }, + { + "name": "EngineRacer::DoShifting", + "path": "src/Speed/Indep/Src/Physics/Behaviors/EngineRacer.cpp", + "lines": [ + 1055, + 1075 + ] + } + ], + "dependsOn": [ + "racer-chassis" + ] + }, + { + "id": "police-pursuit", + "name": "Police pursuit activity", + "purpose": "One pursuit per wanted target: heat escalation over time, the arrest meter, line-of-sight loss and cool-down evasion, and the ramming formations that police units try to close around the target", + "paths": [ + "src/Speed/Indep/Src/AI/Common/AIPursuit.cpp" + ], + "functions": [ + { + "name": "BoxInFormation::BoxInFormation", + "path": "src/Speed/Indep/Src/AI/Common/AIPursuit.cpp", + "lines": [ + 179, + 218 + ] + }, + { + "name": "BoxInFormation::getPosition", + "path": "src/Speed/Indep/Src/AI/Common/AIPursuit.cpp", + "lines": [ + 220, + 227 + ] + }, + { + "name": "BoxInFormation::Update", + "path": "src/Speed/Indep/Src/AI/Common/AIPursuit.cpp", + "lines": [ + 229, + 239 + ] + }, + { + "name": "AIPursuit::AIPursuit", + "path": "src/Speed/Indep/Src/AI/Common/AIPursuit.cpp", + "lines": [ + 543, + 682 + ] + }, + { + "name": "AIPursuit::UpdateFormation", + "path": "src/Speed/Indep/Src/AI/Common/AIPursuit.cpp", + "lines": [ + 1340, + 1537 + ] + }, + { + "name": "AIPursuit::OnTask", + "path": "src/Speed/Indep/Src/AI/Common/AIPursuit.cpp", + "lines": [ + 1619, + 2125 + ] + } + ], + "dependsOn": [ + "racer-chassis" + ] + }, + { + "id": "traffic", + "name": "Civilian traffic", + "purpose": "Density-driven spawning of civilian cars ahead of the camera, despawn validation, and the lane-following speed controller that slows for curves and for blocked lanes", + "paths": [ + "src/Speed/Indep/Src/AI/Activities/AITrafficManager.cpp", + "src/Speed/Indep/Src/AI/Actions/AIActionTraffic.cpp" + ], + "functions": [ + { + "name": "AITrafficManager::FindCollisions", + "path": "src/Speed/Indep/Src/AI/Activities/AITrafficManager.cpp", + "lines": [ + 436, + 468 + ] + }, + { + "name": "AITrafficManager::FindSpawnPoint", + "path": "src/Speed/Indep/Src/AI/Activities/AITrafficManager.cpp", + "lines": [ + 489, + 534 + ] + }, + { + "name": "AITrafficManager::ComputeDensity", + "path": "src/Speed/Indep/Src/AI/Activities/AITrafficManager.cpp", + "lines": [ + 625, + 653 + ] + }, + { + "name": "AITrafficManager::Update", + "path": "src/Speed/Indep/Src/AI/Activities/AITrafficManager.cpp", + "lines": [ + 655, + 674 + ] + }, + { + "name": "AIActionTraffic::ComputeSpeed", + "path": "src/Speed/Indep/Src/AI/Actions/AIActionTraffic.cpp", + "lines": [ + 205, + 251 + ] + }, + { + "name": "AIActionTraffic::Update", + "path": "src/Speed/Indep/Src/AI/Actions/AIActionTraffic.cpp", + "lines": [ + 302, + 389 + ] + } + ], + "dependsOn": [ + "race-ai" + ] + }, + { + "id": "race-ai", + "name": "Opponent race driver", + "purpose": "Target-speed planning for opponent racers on the road network: curvature speed limits, skill scaling, catch-up acceleration and nitrous decisions that feed the throttle and steering controllers", + "paths": [ + "src/Speed/Indep/Src/AI/Actions/AIActionRace.cpp" + ], + "functions": [ + { + "name": "GetSpeedLimitForCurvature", + "path": "src/Speed/Indep/Src/AI/Actions/AIActionRace.cpp", + "lines": [ + 466, + 473 + ] + }, + { + "name": "AIActionRace::GetPotentialSpeed", + "path": "src/Speed/Indep/Src/AI/Actions/AIActionRace.cpp", + "lines": [ + 485, + 597 + ] + }, + { + "name": "AIActionRace::GetPotentialAcceleration", + "path": "src/Speed/Indep/Src/AI/Actions/AIActionRace.cpp", + "lines": [ + 635, + 671 + ] + } + ], + "dependsOn": [ + "racer-engine" + ] + } + ], + "cards": [ + { + "id": "tire-lateral-grip", + "node": "racer-chassis", + "name": "Tire lateral grip curve", + "summary": "Cornering force read from load-sensitivity curves picked by slip angle, scaled by a per-axle grip value", + "behavior": "Each tire converts its slip angle to degrees and halves it, so every curve covers a two-degree step and the fractional part blends between two neighbouring curves. Seven curves run from zero to twelve degrees. Past that, the twelve-degree curve applies flat, which caps cornering force. Each curve maps load in kilonewtons, after a 0.8 load factor, over 0 to 10 kN to a force that rises steeply and then flattens, so heavier load gives less extra grip. The result is multiplied by the axle grip scale, a cornering scale of 1000, a grip factor of 2.5 and any temporary grip boost. Undriven rear wheels below 30 mph get a pilot factor of 0.85 that blends up to full grip by 50 mph.", + "constants": [ + { + "name": "slipCurveSpacing", + "value": 2, + "unit": "degrees of slip angle per curve, static", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:33" + }, + { + "name": "loadCurveMaxLoad", + "value": 10, + "unit": "kilonewtons, static", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:34" + }, + { + "name": "gripCurveSlipCeiling", + "value": 12, + "unit": "degrees, static", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:61" + }, + { + "name": "lateralForceScale", + "value": 1000, + "unit": "multiplier per update, rate unverified", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:549" + }, + { + "name": "loadFactor", + "value": 0.8, + "unit": "multiplier on load per update, rate unverified", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:551" + }, + { + "name": "gripFactor", + "value": 2.5, + "unit": "multiplier per update, rate unverified", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:552" + }, + { + "name": "pilotFactor", + "value": 0.85, + "unit": "grip multiplier below 30 mph, per update, rate unverified", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:583" + } + ], + "sources": [ + { + "path": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp", + "line": 38 + }, + { + "path": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp", + "line": 555 + }, + { + "path": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp", + "line": 559 + }, + { + "path": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp", + "line": 582 + }, + { + "path": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp", + "line": 595 + } + ], + "capabilities": [ + "slip angle grip", + "load sensitivity", + "tire model" + ], + "tags": [ + "physics", + "tires", + "grip", + "cornering" + ] + }, + { + "id": "speed-sensitive-steering", + "node": "racer-chassis", + "name": "Speed-sensitive steering lock", + "summary": "Maximum front wheel angle shrinks with forward speed, widens under braking, and opens to match rear slip when counter-steering", + "behavior": "The steering lock for gamepad input comes from a ten-entry table spread over forward speeds from 0 to 160: it starts at 40 degrees and falls to 2.9 degrees at the top. Braking and the handbrake widen the lock by up to a 1.45 multiplier, weighted by a speed table. The average of recent stick input scales the lock up to 1.4 times for sustained hard turns. The car tuning slider adds up to 20 percent per unit. When the player steers into a rear-tire slide, the lock grows to at least the rear slip angle so the player can catch the slide. A hard ceiling of 45 degrees always applies.", + "constants": [ + { + "name": "absoluteMaxSteering", + "value": 45, + "unit": "degrees, static", + "source": "src/Speed/Indep/Src/Physics/CarBasics.hpp:9" + }, + { + "name": "lowSpeedSteeringLock", + "value": 40, + "unit": "degrees at zero forward speed, static", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:1077" + }, + { + "name": "highSpeedSteeringLock", + "value": 2.9, + "unit": "degrees at top of speed table, static", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:1077" + }, + { + "name": "steeringTableMaxSpeed", + "value": 160, + "unit": "forward speed in physics velocity units, meters per second assumed, unverified", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:1070" + }, + { + "name": "brakingLockWidening", + "value": 1.45, + "unit": "multiplier per update, rate unverified", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:1072" + }, + { + "name": "sustainedInputLockBoost", + "value": 1.4, + "unit": "multiplier at full averaged input, per update, rate unverified", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:1082" + }, + { + "name": "tuningSteeringRatio", + "value": 0.2, + "unit": "multiplier per tuning unit, static", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:1217" + } + ], + "sources": [ + { + "path": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp", + "line": 1103 + }, + { + "path": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp", + "line": 1221 + }, + { + "path": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp", + "line": 1234 + }, + { + "path": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp", + "line": 1258 + } + ], + "capabilities": [ + "speed-sensitive steering", + "counter-steer assist", + "brake steering" + ], + "tags": [ + "steering", + "handling", + "movement", + "input" + ] + }, + { + "id": "steering-rate", + "node": "racer-chassis", + "name": "Steering slew rate and input smoothing", + "summary": "Gamepad steering turns the wheels at a capped rate that slows with speed and quickens with fast stick motion", + "behavior": "On a gamepad the front wheel angle moves toward the target at a base rate of 180 degrees per second. A speed table cuts that rate to 0.3 of the base at high speed. A second table measures how fast the stick itself moves and raises the rate up to 3.1 times for snap inputs, averaged over a 0.15 second window. The 0.55 second running average of stick position adds up to 1.4 times more for held turns. The step for one update is the rate times the update time, and the new angle is clamped to the previous angle plus or minus that step. Racing wheels skip the slew limit.", + "constants": [ + { + "name": "baseWheelSlewRate", + "value": 180, + "unit": "degrees per second", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:1114" + }, + { + "name": "highSpeedSteeringSpeedScale", + "value": 0.3, + "unit": "multiplier at top of speed table, static", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:1079" + }, + { + "name": "fastStickSteeringBoost", + "value": 3.1, + "unit": "multiplier at highest stick speed, static", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:1081" + }, + { + "name": "stickSpeedAverageWindow", + "value": 0.15, + "unit": "seconds", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:64" + }, + { + "name": "stickPositionAverageWindow", + "value": 0.55, + "unit": "seconds", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:63" + } + ], + "sources": [ + { + "path": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp", + "line": 1168 + }, + { + "path": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp", + "line": 1196 + }, + { + "path": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp", + "line": 1273 + }, + { + "path": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp", + "line": 433 + } + ], + "capabilities": [ + "steering rate limit", + "input smoothing", + "gamepad steering" + ], + "tags": [ + "steering", + "input", + "handling", + "controls" + ] + }, + { + "id": "post-collision-steering", + "node": "racer-chassis", + "name": "Post-collision steering reduction", + "summary": "Steering lock drops for one second after a hard impact and recovers along a short curve, less so at high speed", + "behavior": "A collision impulse starts a one-second timer. During it, the steering lock is multiplied by a recovery curve that starts at 0.2 of normal and climbs back to full by the end of the second. The effect fades with speed: the reduction is fully applied at a standstill and vanishes at 70 percent of 170 mph. This keeps a car from spinning itself after a wall hit at low speed while leaving high-speed control intact.", + "constants": [ + { + "name": "steerReductionDuration", + "value": 1, + "unit": "seconds", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:1061" + }, + { + "name": "initialSteerFraction", + "value": 0.2, + "unit": "fraction of steering lock at impact, static", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:1062" + }, + { + "name": "reductionFadeSpeed", + "value": 170, + "unit": "mph, multiplied by 0.7", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:1247" + }, + { + "name": "reductionFadeSpeedFraction", + "value": 0.7, + "unit": "fraction of fade speed, static", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:1247" + } + ], + "sources": [ + { + "path": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp", + "line": 1063 + }, + { + "path": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp", + "line": 1244 + }, + { + "path": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp", + "line": 1250 + } + ], + "capabilities": [ + "collision recovery", + "steering damping" + ], + "tags": [ + "steering", + "collision", + "handling" + ] + }, + { + "id": "arcade-drift", + "node": "racer-chassis", + "name": "Arcade drift state", + "summary": "A drift value rises fast when sliding with throttle or handbrake and lowers rear grip while a stabilizer torque keeps the slide controllable", + "behavior": "A drift value between 0 and 1 rises at 8 per second while drifting and falls at 2 per second otherwise. A drift starts above 30 mph when the handbrake passes half travel or the body slip angle exceeds 12 degrees. It holds while throttle times steer times slip stays past 12 degrees, or while the player counter-steers into the slide. While the value is above zero, a yaw damping torque opposes rotation, scaled by the slip angle through a stabilizer curve times 4. Rear tire friction comes from a ten-entry table, from 1.1 down to 0.34. Its input sums half the slip angle, half the yaw rate and 4 times the counter-steer amount, all times the drift value.", + "constants": [ + { + "name": "driftRiseRate", + "value": 8, + "unit": "drift value per second", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:1485" + }, + { + "name": "driftFallRate", + "value": -2, + "unit": "drift value per second", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:1490" + }, + { + "name": "minDriftSpeed", + "value": 30, + "unit": "mph", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:1462" + }, + { + "name": "driftSlipAngle", + "value": 12, + "unit": "degrees", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:1463" + }, + { + "name": "handbrakeDriftThreshold", + "value": 0.5, + "unit": "handbrake input fraction, static", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:1521" + }, + { + "name": "slideDampingGain", + "value": 4, + "unit": "multiplier per update, rate unverified", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:1460" + }, + { + "name": "yawAngularVelocityCoefficient", + "value": 0.5, + "unit": "multiplier per update, rate unverified", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:1461" + }, + { + "name": "counterSteerShare", + "value": 4, + "unit": "multiplier per update, rate unverified", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:1466" + }, + { + "name": "maxRearDriftFriction", + "value": 1.1, + "unit": "rear friction multiplier at zero drift input, static", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:1457" + }, + { + "name": "minRearDriftFriction", + "value": 0.34, + "unit": "rear friction multiplier at full drift input, static", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:1457" + } + ], + "sources": [ + { + "path": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp", + "line": 1474 + }, + { + "path": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp", + "line": 1520 + }, + { + "path": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp", + "line": 1546 + }, + { + "path": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp", + "line": 1556 + } + ], + "capabilities": [ + "drift", + "yaw stabilizer", + "rear grip loss" + ], + "tags": [ + "drift", + "handling", + "physics", + "arcade" + ] + }, + { + "id": "burnout-fishtail", + "node": "racer-chassis", + "name": "Burnout and fishtail launch", + "summary": "Wheelspin at low speed starts a burnout that cuts drive traction and steps the car through several fishtail swings", + "behavior": "Below 20 mph, a drive wheel slip above 0.5 starts a burnout. Drive traction is set from a slip-to-friction curve, which ranges from 1.0 at no slip down to 0.65 at high slip, divided by 1.4. The swing count is 6 times the gap between 1.5 and that friction value, and the slipping side adds one more step that sets the swing direction. Each swing lasts 2 seconds before the counter steps down. The burnout ends early when slip stays under 0.5 for one second, or when the car is above 5 mph and turned more than 0.5 radians off its heading. The differential bias follows the burnout traction so the rear steps out.", + "constants": [ + { + "name": "burnoutMaxSpeed", + "value": 20, + "unit": "mph", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:1324" + }, + { + "name": "burnoutEndSlip", + "value": 0.5, + "unit": "wheel slip ratio, static", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:1321" + }, + { + "name": "burnoutEndHeadingError", + "value": 0.5, + "unit": "radians", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:1322" + }, + { + "name": "lowSlipGraceTime", + "value": 1, + "unit": "seconds", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:1323" + }, + { + "name": "fishtailSwingTime", + "value": 2, + "unit": "seconds", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:1325" + }, + { + "name": "maxFishtailSwings", + "value": 6, + "unit": "swings, static", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:1326" + }, + { + "name": "burnoutFrictionDivisor", + "value": 1.4, + "unit": "divisor, static", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:1350" + }, + { + "name": "highSlipBurnoutFriction", + "value": 0.65, + "unit": "friction multiplier at highest slip, static", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:1319" + } + ], + "sources": [ + { + "path": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp", + "line": 1330 + }, + { + "path": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp", + "line": 1333 + }, + { + "path": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp", + "line": 1352 + }, + { + "path": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp", + "line": 1773 + } + ], + "capabilities": [ + "burnout", + "fishtail", + "launch wheelspin" + ], + "tags": [ + "launch", + "traction", + "physics", + "handling" + ] + }, + { + "id": "yaw-assist-handbrake", + "node": "racer-chassis", + "name": "Rear yaw assist and handbrake turn", + "summary": "Rear tires get extra friction against yaw to keep the car pointed, except during a deliberate handbrake slide", + "behavior": "Each rear tire gets a friction boost that grows with the car yaw angle, the per-car yaw control value and speed up to 30. The bonus is capped at 0.35, and the road grade adds to it. The boost is skipped while the handbrake is above half and the yaw is still under 20 degrees, so the player can start a slide freely. A sharp handbrake turn below 80 mph with yaw above 0.3 radians adds 0.5 to rear brake force, which produces a fast 180-degree turn. The yaw control fades as the drift value rises. A blown tire has traction scaled to 0.3.", + "constants": [ + { + "name": "yawFrictionBonusCap", + "value": 0.35, + "unit": "friction multiplier bonus, static", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:1407" + }, + { + "name": "yawAssistFullSpeed", + "value": 30, + "unit": "speed in physics velocity units, meters per second assumed, unverified", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:1406" + }, + { + "name": "handbrakeYawThreshold", + "value": 0.5, + "unit": "handbrake input fraction, static", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:1410" + }, + { + "name": "handbrakeYawAngle", + "value": 20, + "unit": "degrees", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:1411" + }, + { + "name": "handbrakeTurnYaw", + "value": 0.3, + "unit": "radians", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:1403" + }, + { + "name": "handbrakeTurnMaxSpeed", + "value": 80, + "unit": "mph", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:1404" + }, + { + "name": "handbrakeTurnExtraBrake", + "value": 0.5, + "unit": "brake input added per update, rate unverified", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:1632" + }, + { + "name": "blownTireTraction", + "value": 0.3, + "unit": "traction multiplier, static", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:1569" + } + ], + "sources": [ + { + "path": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp", + "line": 1414 + }, + { + "path": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp", + "line": 1572 + }, + { + "path": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp", + "line": 1594 + }, + { + "path": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp", + "line": 1643 + } + ], + "capabilities": [ + "yaw stability", + "handbrake turn", + "tire friction boost" + ], + "tags": [ + "handling", + "handbrake", + "stability", + "physics" + ] + }, + { + "id": "traction-control", + "node": "racer-chassis", + "name": "Cornering traction control", + "summary": "Drive torque shifts away from the inside wheel when the driven axle slips sideways under power", + "behavior": "When the driven axle slides sideways in the steering direction and torque pushes forward slip, a limit ramps in between 1 and 20 units of forward slip excess. It is weighted again by the steer-scaled slip direction, which ramps in between 1 and 16 degrees. The wheel on the sliding side loses that fraction of drive torque, the other wheel loses half, and the outside wheel gains the same fraction as extra traction. Burnouts lock the differential instead and skip this control.", + "constants": [ + { + "name": "slideAngleRampEnd", + "value": 16, + "unit": "degrees", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:1720" + }, + { + "name": "forwardSlipRampEnd", + "value": 20, + "unit": "forward slip speed, meters per second assumed, unverified", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:1721" + }, + { + "name": "outsideWheelTorqueShare", + "value": 0.5, + "unit": "fraction of limit removed from the other wheel, per update, rate unverified", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:1791" + } + ], + "sources": [ + { + "path": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp", + "line": 1724 + }, + { + "path": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp", + "line": 1783 + }, + { + "path": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp", + "line": 1787 + } + ], + "capabilities": [ + "traction control", + "torque split" + ], + "tags": [ + "drivetrain", + "traction", + "handling" + ] + }, + { + "id": "speedbreaker", + "node": "racer-chassis", + "name": "Slow-motion breaker handling", + "summary": "During the slow-motion breaker the car gains downforce, steering lock, front grip and less drag, then ramps back over three seconds", + "behavior": "While the player holds the breaker, a blend value is 1. After release it falls to 0 over 3 seconds of simulated time. The blend adds downforce equal to 2 g of the car mass and cuts aero drag by up to 75 percent. It also lerps the steering angle toward 60 degrees times stick input and multiplies front tire friction by 1 plus 0.75 times the steer amount. Drifting refills the breaker gauge at half a unit per second times the drift value, but only above 35 mph with more than 30 degrees of yaw and while the breaker is off.", + "constants": [ + { + "name": "extraDownforce", + "value": -2, + "unit": "g along the up vector", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:936" + }, + { + "name": "rampOutTime", + "value": 3, + "unit": "seconds, used as 1 divided by 3 per second", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:937" + }, + { + "name": "dragReduction", + "value": 0.75, + "unit": "fraction of drag removed at full blend, per update, rate unverified", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:988" + }, + { + "name": "breakerMaxSteer", + "value": 60, + "unit": "degrees", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:1122" + }, + { + "name": "frontGripBoost", + "value": 0.75, + "unit": "friction multiplier per unit of steer input, per update, rate unverified", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:1649" + }, + { + "name": "driftRechargeRate", + "value": 0.5, + "unit": "gauge units per second", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:1467" + }, + { + "name": "driftRechargeMinSpeed", + "value": 35, + "unit": "mph", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:1468" + }, + { + "name": "driftRechargeYaw", + "value": 30, + "unit": "degrees", + "source": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp:1469" + } + ], + "sources": [ + { + "path": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp", + "line": 940 + }, + { + "path": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp", + "line": 964 + }, + { + "path": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp", + "line": 1210 + }, + { + "path": "src/Speed/Indep/Src/Physics/Behaviors/SuspensionRacer.cpp", + "line": 1532 + } + ], + "capabilities": [ + "bullet time handling", + "meter recharge", + "downforce" + ], + "tags": [ + "speedbreaker", + "slow motion", + "handling", + "meter" + ] + }, + { + "id": "nitrous-tank", + "node": "racer-engine", + "name": "Nitrous tank", + "summary": "A normalized nitrous tank drains while engaged, waits for a disengage delay, then refills at a speed-dependent rate", + "behavior": "Nitrous engages only in a forward gear with throttle above zero and at 10 mph or more. Once engaged it stays on down to half that speed. While engaged, the tank of capacity 1 drains by update time divided by the car discharge time, and torque is multiplied by the car boost value. After release, a disengage timer must run out before refill starts. Refill time per full tank blends between a minimum and maximum value as speed moves through the car recharge band. Opponent drivers on catch-up drain at down to half rate and refill at up to twice the rate.", + "constants": [ + { + "name": "minNitrousSpeed", + "value": 10, + "unit": "mph", + "source": "src/Speed/Indep/Src/Physics/Behaviors/EngineRacer.cpp:926" + }, + { + "name": "stayEngagedSpeedFraction", + "value": 0.5, + "unit": "fraction of minimum speed, static", + "source": "src/Speed/Indep/Src/Physics/Behaviors/EngineRacer.cpp:950" + }, + { + "name": "catchupRechargeScale", + "value": 2, + "unit": "multiplier on refill per second at full catch-up", + "source": "src/Speed/Indep/Src/Physics/Behaviors/EngineRacer.cpp:927" + }, + { + "name": "catchupDischargeScale", + "value": 0.5, + "unit": "multiplier on drain per second at full catch-up", + "source": "src/Speed/Indep/Src/Physics/Behaviors/EngineRacer.cpp:928" + } + ], + "sources": [ + { + "path": "src/Speed/Indep/Src/Physics/Behaviors/EngineRacer.cpp", + "line": 931 + }, + { + "path": "src/Speed/Indep/Src/Physics/Behaviors/EngineRacer.cpp", + "line": 958 + }, + { + "path": "src/Speed/Indep/Src/Physics/Behaviors/EngineRacer.cpp", + "line": 973 + }, + { + "path": "src/Speed/Indep/Src/Physics/Behaviors/EngineRacer.cpp", + "line": 976 + } + ], + "capabilities": [ + "nitrous boost", + "resource meter", + "catch-up" + ], + "tags": [ + "nitrous", + "boost", + "engine", + "meter" + ] + }, + { + "id": "auto-shift", + "node": "racer-engine", + "name": "Automatic gearbox and engine speed smoothing", + "summary": "Gears shift on rpm thresholds with a traction check, downshift delays are shorter, and displayed rpm falls at a capped rate", + "behavior": "The automatic gearbox upshifts when rpm passes the gear shift-up point, but only when all four wheels touch ground and slip less than 4. Downshifts skip several gears when one would still be too low. When coasting, the downshift point moves 65 percent of the way from idle toward the normal point. A downshift starts a shift timer of a quarter of the normal shift delay. Engine rpm may fall no faster than 2.5 thousand per unit of inertia per second, or 15 thousand while shifting above first gear. Each update then blends 55 percent new rpm with 45 percent old rpm. Above the speed limiter start, throttle fades to zero across the cutoff band.", + "constants": [ + { + "name": "upshiftMaxWheelSlip", + "value": 4, + "unit": "wheel slip, static", + "source": "src/Speed/Indep/Src/Physics/Behaviors/EngineRacer.cpp:636" + }, + { + "name": "coastingDownshiftPercent", + "value": 0.65, + "unit": "fraction between idle and downshift rpm, static", + "source": "src/Speed/Indep/Src/Physics/Behaviors/EngineRacer.cpp:650" + }, + { + "name": "downshiftDelayScale", + "value": 0.25, + "unit": "fraction of shift delay seconds", + "source": "src/Speed/Indep/Src/Physics/Behaviors/EngineRacer.cpp:715" + }, + { + "name": "rpmFallRate", + "value": 2.5, + "unit": "thousand rpm per second per unit engine inertia", + "source": "src/Speed/Indep/Src/Physics/Behaviors/EngineRacer.cpp:885" + }, + { + "name": "rpmFallRateShifting", + "value": 15, + "unit": "thousand rpm per second per unit engine inertia", + "source": "src/Speed/Indep/Src/Physics/Behaviors/EngineRacer.cpp:885" + }, + { + "name": "newRpmBlend", + "value": 0.55, + "unit": "blend weight per update, rate unverified", + "source": "src/Speed/Indep/Src/Physics/Behaviors/EngineRacer.cpp:903" + }, + { + "name": "sportShiftHold", + "value": 1.25, + "unit": "seconds, decays at 2 minus throttle per second", + "source": "src/Speed/Indep/Src/Physics/Behaviors/EngineRacer.cpp:701" + } + ], + "sources": [ + { + "path": "src/Speed/Indep/Src/Physics/Behaviors/EngineRacer.cpp", + "line": 606 + }, + { + "path": "src/Speed/Indep/Src/Physics/Behaviors/EngineRacer.cpp", + "line": 653 + }, + { + "path": "src/Speed/Indep/Src/Physics/Behaviors/EngineRacer.cpp", + "line": 888 + }, + { + "path": "src/Speed/Indep/Src/Physics/Behaviors/EngineRacer.cpp", + "line": 907 + }, + { + "path": "src/Speed/Indep/Src/Physics/Behaviors/EngineRacer.cpp", + "line": 1070 + } + ], + "capabilities": [ + "automatic transmission", + "rpm smoothing", + "speed limiter" + ], + "tags": [ + "engine", + "gearbox", + "transmission", + "audio feel" + ] + }, + { + "id": "pursuit-heat", + "node": "police-pursuit", + "name": "Pursuit heat escalation", + "summary": "Heat rises continuously with pursuit time between a floor and a ceiling, and each whole heat level changes the police response", + "behavior": "Outside cool-down, heat grows by the update time divided by the seconds-per-level value of the current pursuit tier. A career progress bucket, capped at 14, scales that time. Heat is clamped between a base and a maximum. Quick races use 1 and 5. The final story chase pins both base and maximum at 6. Each time the integer part of heat changes, the pursuit resets its formation timer, rechecks support units and loads the reward rate for the new level. Pursuit time also pays reward points once for each completed 10-second block.", + "constants": [ + { + "name": "quickRaceBaseHeat", + "value": 1, + "unit": "heat level, static", + "source": "src/Speed/Indep/Src/AI/Common/AIPursuit.cpp:627" + }, + { + "name": "quickRaceMaxHeat", + "value": 5, + "unit": "heat level, static", + "source": "src/Speed/Indep/Src/AI/Common/AIPursuit.cpp:628" + }, + { + "name": "finalChaseHeat", + "value": 6, + "unit": "heat level, static", + "source": "src/Speed/Indep/Src/AI/Common/AIPursuit.cpp:620" + }, + { + "name": "rewardBlockTime", + "value": 10, + "unit": "seconds", + "source": "src/Speed/Indep/Src/AI/Common/AIPursuit.cpp:36" + } + ], + "sources": [ + { + "path": "src/Speed/Indep/Src/AI/Common/AIPursuit.cpp", + "line": 1619 + }, + { + "path": "src/Speed/Indep/Src/AI/Common/AIPursuit.cpp", + "line": 1684 + }, + { + "path": "src/Speed/Indep/Src/AI/Common/AIPursuit.cpp", + "line": 1680 + }, + { + "path": "src/Speed/Indep/Src/AI/Common/AIPursuit.cpp", + "line": 1918 + } + ], + "capabilities": [ + "wanted level", + "difficulty escalation" + ], + "tags": [ + "police", + "pursuit", + "heat", + "wanted" + ] + }, + { + "id": "pursuit-busted-meter", + "node": "police-pursuit", + "name": "Arrest meter", + "summary": "An arrest timer fills while a slow target sits near a police unit and drains twice as fast otherwise", + "behavior": "While the target is in sight, slower than the tier bust speed and within 15 meters of the nearest police unit on the ground plane, the arrest timer gains a quarter of the update time. Otherwise it loses half the update time, and it never falls below zero. A target that is flashing after a manual reset counts at any speed within six times the distance and fills four times as fast. When the timer passes 5 the target is arrested and the remaining police units are told to leave. The on-screen meter shows the timer times 0.2.", + "constants": [ + { + "name": "arrestRadius", + "value": 15, + "unit": "meters", + "source": "src/Speed/Indep/Src/AI/Common/AIPursuit.cpp:1608" + }, + { + "name": "arrestThreshold", + "value": 5, + "unit": "arrest timer units, unitless", + "source": "src/Speed/Indep/Src/AI/Common/AIPursuit.cpp:1607" + }, + { + "name": "bustedFillRate", + "value": 0.25, + "unit": "timer units per second of update time", + "source": "src/Speed/Indep/Src/AI/Common/AIPursuit.cpp:1905" + }, + { + "name": "bustedDrainRate", + "value": -0.5, + "unit": "timer units per second of update time", + "source": "src/Speed/Indep/Src/AI/Common/AIPursuit.cpp:1911" + }, + { + "name": "resetDistanceScale", + "value": 6, + "unit": "multiplier on bust distance, static", + "source": "src/Speed/Indep/Src/AI/Common/AIPursuit.cpp:1901" + }, + { + "name": "resetFillScale", + "value": 4, + "unit": "multiplier on fill rate, static", + "source": "src/Speed/Indep/Src/AI/Common/AIPursuit.cpp:1908" + }, + { + "name": "meterDisplayScale", + "value": 0.2, + "unit": "display fraction per timer unit, static", + "source": "src/Speed/Indep/Src/AI/Common/AIPursuit.cpp:2655" + } + ], + "sources": [ + { + "path": "src/Speed/Indep/Src/AI/Common/AIPursuit.cpp", + "line": 1621 + }, + { + "path": "src/Speed/Indep/Src/AI/Common/AIPursuit.cpp", + "line": 1887 + }, + { + "path": "src/Speed/Indep/Src/AI/Common/AIPursuit.cpp", + "line": 1940 + } + ], + "capabilities": [ + "arrest condition", + "proximity meter" + ], + "tags": [ + "police", + "pursuit", + "busted", + "meter" + ] + }, + { + "id": "pursuit-evade-cooldown", + "node": "police-pursuit", + "name": "Line of sight loss and cool-down evasion", + "summary": "After seven seconds unseen the chase enters cool-down, and hiding spots fill the evade meter faster", + "behavior": "The target counts as seen while any police unit saw it in the last 7 seconds. Past that, the pursuit meter switches mode after 2.5 seconds, enters cool-down, and flags a helicopter search check. The evade level is the unseen time plus bonus hidden time, divided by the cool-down time required, which is 60 seconds by default and comes from the tier. Inside a hiding spot, hidden time accrues at three times the update time when the tier gives no multiplier. Being seen again clears the hidden time and records the last known position. The level shows at least 0.05 and evasion completes at 1.", + "constants": [ + { + "name": "lineOfSightMemory", + "value": 7, + "unit": "seconds", + "source": "src/Speed/Indep/Src/AI/Common/AIPursuit.cpp:2000" + }, + { + "name": "fallbackEvadeTime", + "value": 60, + "unit": "seconds", + "source": "src/Speed/Indep/Src/AI/Common/AIPursuit.cpp:558" + }, + { + "name": "hidingSpotMultiplier", + "value": 3, + "unit": "multiplier on hidden time per second", + "source": "src/Speed/Indep/Src/AI/Common/AIPursuit.cpp:2015" + }, + { + "name": "meterModeSwitchDelay", + "value": 2.5, + "unit": "seconds", + "source": "src/Speed/Indep/Src/AI/Common/AIPursuit.cpp:2038" + }, + { + "name": "minimumShownEvadeLevel", + "value": 0.05, + "unit": "evade fraction, static", + "source": "src/Speed/Indep/Src/AI/Common/AIPursuit.cpp:2052" + } + ], + "sources": [ + { + "path": "src/Speed/Indep/Src/AI/Common/AIPursuit.cpp", + "line": 2029 + }, + { + "path": "src/Speed/Indep/Src/AI/Common/AIPursuit.cpp", + "line": 2033 + }, + { + "path": "src/Speed/Indep/Src/AI/Common/AIPursuit.cpp", + "line": 2114 + } + ], + "capabilities": [ + "stealth escape", + "cool-down timer", + "last known position" + ], + "tags": [ + "police", + "pursuit", + "evade", + "stealth" + ] + }, + { + "id": "pursuit-box-in", + "node": "police-pursuit", + "name": "Box-in ramming formation", + "summary": "Four police units take slots ahead, beside and behind the target, then pull the box tighter as the finisher timer runs", + "behavior": "The box uses four slots relative to the target: 14 meters ahead, 3.5 meters to each side, and 7.5 meters behind. Each slot has a ramming goal. A tightness value, 0.5 by default, sets how far slots shrink once units are in place. As the time-to-finisher ratio grows, slot offsets scale from 1 toward 1 minus a factor of 0.2 plus 0.2 times tightness. The finisher phase lasts 2 seconds by default. Formations collapse only below a tier collapse speed, which is set to 125 km/h for a target that drives aggressively into police.", + "constants": [ + { + "name": "frontSlotDistance", + "value": 14, + "unit": "meters", + "source": "src/Speed/Indep/Src/AI/Common/AIPursuit.cpp:224" + }, + { + "name": "sideSlotOffset", + "value": 3.5, + "unit": "meters", + "source": "src/Speed/Indep/Src/AI/Common/AIPursuit.cpp:224" + }, + { + "name": "rearSlotDistance", + "value": -7.5, + "unit": "meters", + "source": "src/Speed/Indep/Src/AI/Common/AIPursuit.cpp:224" + }, + { + "name": "defaultTightness", + "value": 0.5, + "unit": "fraction, static", + "source": "src/Speed/Indep/Src/AI/Common/AIPursuit.cpp:189" + }, + { + "name": "defaultFinisherTime", + "value": 2, + "unit": "seconds", + "source": "src/Speed/Indep/Src/AI/Common/AIPursuit.cpp:190" + }, + { + "name": "aggressiveCollapseSpeed", + "value": 125, + "unit": "km/h", + "source": "src/Speed/Indep/Src/AI/Common/AIPursuit.cpp:1448" + } + ], + "sources": [ + { + "path": "src/Speed/Indep/Src/AI/Common/AIPursuit.cpp", + "line": 179 + }, + { + "path": "src/Speed/Indep/Src/AI/Common/AIPursuit.cpp", + "line": 229 + }, + { + "path": "src/Speed/Indep/Src/AI/Common/AIPursuit.cpp", + "line": 1340 + } + ], + "capabilities": [ + "formation steering", + "ram attack", + "group AI" + ], + "tags": [ + "police", + "formation", + "AI", + "pursuit" + ] + }, + { + "id": "traffic-spawning", + "node": "traffic", + "name": "Traffic spawn density", + "summary": "Civilian cars spawn about 200 meters ahead of the camera at a rate set by density, with oncoming bias at low speed", + "behavior": "Traffic density is the race setting as a fraction, or 1 while free roaming, and drops to 75 percent during any pursuit. Density maps through an eleven-step table to a spawn rate that reaches 8 at full density. Spawn points sit along the camera heading, rotated by a small random yaw. Their distance is 200 meters plus or minus 50 plus the camera forward speed. The chance of an oncoming car falls from 1 at rest to 0.5 at 50 meters per second. A point is rejected within 150 meters of any traffic center or 20 meters of any active car on the ground plane.", + "constants": [ + { + "name": "maxSpawnRate", + "value": 8, + "unit": "spawn rate multiplier on update time at full density", + "source": "src/Speed/Indep/Src/AI/Activities/AITrafficManager.cpp:47" + }, + { + "name": "spawnDistance", + "value": 200, + "unit": "meters", + "source": "src/Speed/Indep/Src/AI/Activities/AITrafficManager.cpp:513" + }, + { + "name": "spawnJitter", + "value": 50, + "unit": "meters", + "source": "src/Speed/Indep/Src/AI/Activities/AITrafficManager.cpp:511" + }, + { + "name": "pursuitDensityScale", + "value": 0.75, + "unit": "multiplier, static", + "source": "src/Speed/Indep/Src/AI/Activities/AITrafficManager.cpp:648" + }, + { + "name": "minCenterDistanceSquared", + "value": 22500, + "unit": "square meters", + "source": "src/Speed/Indep/Src/AI/Activities/AITrafficManager.cpp:451" + }, + { + "name": "minCarDistanceSquared", + "value": 400, + "unit": "square meters", + "source": "src/Speed/Indep/Src/AI/Activities/AITrafficManager.cpp:463" + }, + { + "name": "oncomingFullSpeed", + "value": 50, + "unit": "meters per second", + "source": "src/Speed/Indep/Src/AI/Activities/AITrafficManager.cpp:518" + } + ], + "sources": [ + { + "path": "src/Speed/Indep/Src/AI/Activities/AITrafficManager.cpp", + "line": 489 + }, + { + "path": "src/Speed/Indep/Src/AI/Activities/AITrafficManager.cpp", + "line": 625 + }, + { + "path": "src/Speed/Indep/Src/AI/Activities/AITrafficManager.cpp", + "line": 655 + } + ], + "capabilities": [ + "traffic spawning", + "density control", + "streaming population" + ], + "tags": [ + "traffic", + "spawning", + "open world", + "AI" + ] + }, + { + "id": "traffic-speed-control", + "node": "traffic", + "name": "Traffic lane speed control", + "summary": "Civilian cars hold a posted speed, slow for curves with low friction, and brake for blocked lanes from a stopping-distance table", + "behavior": "Traffic cruises at 35 mph on normal roads and 55 mph on roads with four or more lanes. The curve speed limit comes from the square root of friction times 9.8 divided by path curvature. Traffic uses a friction of 0.6 and police use 1.6, so police take corners much faster. When the lane ahead is blocked, a stopping distance runs from 3 to 50 meters across speeds of 0 to 80, scaled up for mass over 2000. The car slows in proportion to the remaining gap. The target speed of a civilian car may exceed its current speed by at most twice the update time, which caps acceleration near 2 meters per second squared. Look-ahead grows from 10 to 30 meters as speed rises to 25.", + "constants": [ + { + "name": "cityCruiseSpeed", + "value": 35, + "unit": "mph", + "source": "src/Speed/Indep/Src/AI/Actions/AIActionTraffic.cpp:90" + }, + { + "name": "highwayCruiseSpeed", + "value": 55, + "unit": "mph", + "source": "src/Speed/Indep/Src/AI/Actions/AIActionTraffic.cpp:91" + }, + { + "name": "trafficCurveFriction", + "value": 0.6, + "unit": "friction coefficient, static", + "source": "src/Speed/Indep/Src/AI/Actions/AIActionTraffic.cpp:223" + }, + { + "name": "policeCurveFriction", + "value": 1.6, + "unit": "friction coefficient, static", + "source": "src/Speed/Indep/Src/AI/Actions/AIActionTraffic.cpp:223" + }, + { + "name": "curveGravity", + "value": 9.8, + "unit": "meters per second squared", + "source": "src/Speed/Indep/Src/AI/Actions/AIActionRace.cpp:467" + }, + { + "name": "maxStoppingDistance", + "value": 50, + "unit": "meters at speed 80", + "source": "src/Speed/Indep/Src/AI/Actions/AIActionTraffic.cpp:200" + }, + { + "name": "maxLookAhead", + "value": 30, + "unit": "meters", + "source": "src/Speed/Indep/Src/AI/Actions/AIActionTraffic.cpp:311" + } + ], + "sources": [ + { + "path": "src/Speed/Indep/Src/AI/Actions/AIActionTraffic.cpp", + "line": 205 + }, + { + "path": "src/Speed/Indep/Src/AI/Actions/AIActionTraffic.cpp", + "line": 237 + }, + { + "path": "src/Speed/Indep/Src/AI/Actions/AIActionRace.cpp", + "line": 466 + } + ], + "capabilities": [ + "lane following", + "curve speed limit", + "obstacle braking" + ], + "tags": [ + "traffic", + "AI", + "driving", + "speed" + ] + }, + { + "id": "race-ai-catchup", + "node": "race-ai", + "name": "Opponent acceleration and catch-up", + "summary": "Opponent target acceleration blends toward the player car, scales with skill, and doubles at full catch-up", + "behavior": "An opponent plans acceleration by blending the slowest player car toward its own car by a performance bias. The result is scaled by the car multiplier and doubled when a pursuit-mode racer is actively racing, or by 1.5 when the driver is hunting an aggressive target. Skill maps acceleration from 0.65 to 1.0 and speed from 0.85 to 1.0. Cornering scale runs from 0.36 to 0.9. A catch-up value from 0 to 1 maps through four steps to an acceleration multiplier from 1 to 2. Gravity along the forward slope is added at 9.81. Nitrous adds the car boost on top. The result never falls below zero.", + "constants": [ + { + "name": "maxCatchupAcceleration", + "value": 2, + "unit": "multiplier, static", + "source": "src/Speed/Indep/Src/AI/Actions/AIActionRace.cpp:70" + }, + { + "name": "minSkillAccelerationScale", + "value": 0.65, + "unit": "multiplier at skill 0, static", + "source": "src/Speed/Indep/Src/AI/Actions/AIActionRace.cpp:64" + }, + { + "name": "minSkillSpeedScale", + "value": 0.85, + "unit": "multiplier at skill 0, static", + "source": "src/Speed/Indep/Src/AI/Actions/AIActionRace.cpp:57" + }, + { + "name": "minSkillCorneringScale", + "value": 0.36, + "unit": "multiplier at skill 0, static", + "source": "src/Speed/Indep/Src/AI/Actions/AIActionRace.cpp:77" + }, + { + "name": "pursuitRaceAccelerationScale", + "value": 2, + "unit": "multiplier, static", + "source": "src/Speed/Indep/Src/AI/Actions/AIActionRace.cpp:643" + }, + { + "name": "aggressorAccelerationScale", + "value": 1.5, + "unit": "multiplier, static", + "source": "src/Speed/Indep/Src/AI/Actions/AIActionRace.cpp:646" + }, + { + "name": "slopeGravity", + "value": 9.81, + "unit": "meters per second squared", + "source": "src/Speed/Indep/Src/AI/Actions/AIActionRace.cpp:659" + } + ], + "sources": [ + { + "path": "src/Speed/Indep/Src/AI/Actions/AIActionRace.cpp", + "line": 635 + }, + { + "path": "src/Speed/Indep/Src/AI/Actions/AIActionRace.cpp", + "line": 665 + }, + { + "path": "src/Speed/Indep/Src/AI/Actions/AIActionRace.cpp", + "line": 668 + }, + { + "path": "src/Speed/Indep/Src/AI/Actions/AIActionRace.cpp", + "line": 485 + } + ], + "capabilities": [ + "rubber banding", + "difficulty scaling", + "AI speed planning" + ], + "tags": [ + "AI", + "racing", + "catch-up", + "difficulty" + ] + } + ] +} diff --git a/data/games/need-for-speed.json b/data/games/need-for-speed.json new file mode 100644 index 0000000..28877c8 --- /dev/null +++ b/data/games/need-for-speed.json @@ -0,0 +1,1926 @@ +{ + "version": 1, + "id": "need-for-speed", + "title": "Need for Speed", + "repo": "https://github.com/OpenNFS/OpenNFS", + "sha": "dcd9197033272f8be307a839105e74185809f9f9", + "nodes": [ + { + "id": "physics-world", + "name": "Physics world and stepping", + "purpose": "Bullet rigid-body world with standard gravity, the per-frame input and raycast pass, the substepped simulation and the pre-tick hook that drives each car handling model", + "paths": [ + "src/Physics/PhysicsManager.cpp", + "src/Physics/Car.cpp" + ], + "functions": [ + { + "name": "PhysicsManager", + "path": "src/Physics/PhysicsManager.cpp", + "lines": [ + 33, + 66 + ] + }, + { + "name": "OnPrePhysicsTick", + "path": "src/Physics/PhysicsManager.cpp", + "lines": [ + 72, + 76 + ] + }, + { + "name": "StepSimulation", + "path": "src/Physics/PhysicsManager.cpp", + "lines": [ + 78, + 97 + ] + }, + { + "name": "RegisterVehicle", + "path": "src/Physics/PhysicsManager.cpp", + "lines": [ + 114, + 156 + ] + }, + { + "name": "PhysicsTick", + "path": "src/Physics/Car.cpp", + "lines": [ + 61, + 69 + ] + }, + { + "name": "ApplyAccelerationForce", + "path": "src/Physics/Car.cpp", + "lines": [ + 71, + 94 + ] + }, + { + "name": "_ApplyInputs", + "path": "src/Physics/Car.cpp", + "lines": [ + 255, + 299 + ] + } + ], + "dependsOn": [] + }, + { + "id": "nfs4-handling", + "name": "High Stakes handling model", + "purpose": "Recreated fourth-game PC handling: per-tick input ramps, steering angle, wheel forces summed into a chassis velocity and yaw change, then the stabilizer passes. A port of a community re-implementation, not original code", + "paths": [ + "src/Physics/NFS4VehiclePhysics.cpp", + "src/Physics/NFS4VehiclePhysics.h" + ], + "functions": [ + { + "name": "Update", + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "lines": [ + 1112, + 1275 + ] + }, + { + "name": "ProcessSteeringInput", + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "lines": [ + 686, + 709 + ] + }, + { + "name": "ProcessThrottleInput", + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "lines": [ + 711, + 721 + ] + }, + { + "name": "ProcessBrakeInput", + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "lines": [ + 723, + 741 + ] + }, + { + "name": "SlipAngleFactor", + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "lines": [ + 256, + 286 + ] + }, + { + "name": "CalculateSteeringAngle", + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "lines": [ + 288, + 308 + ] + } + ], + "dependsOn": [ + "physics-world" + ] + }, + { + "id": "nfs4-powertrain", + "parent": "nfs4-handling", + "name": "Engine, gears and drag", + "purpose": "Torque curve lookup, per-gear effective ratio and force clamp, the engine-speed traction state machine, gear shift delay and the cubic drag that sets top speed", + "paths": [ + "src/Physics/NFS4VehiclePhysics.cpp", + "src/Physics/NFS4VehiclePhysics.h" + ], + "functions": [ + { + "name": "TorqueForRPM", + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "lines": [ + 169, + 175 + ] + }, + { + "name": "GearEffectiveRatio", + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "lines": [ + 177, + 182 + ] + }, + { + "name": "TractionPowertrain", + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "lines": [ + 184, + 198 + ] + }, + { + "name": "LongitudinalDragCoefficient", + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "lines": [ + 200, + 213 + ] + }, + { + "name": "CalculateDrag", + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "lines": [ + 215, + 227 + ] + }, + { + "name": "ProcessGearInput", + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "lines": [ + 743, + 753 + ] + }, + { + "name": "ProcessTractionModel", + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "lines": [ + 1277, + 1491 + ] + }, + { + "name": "ApplyGearChange", + "path": "src/Physics/Car.cpp", + "lines": [ + 104, + 110 + ] + } + ], + "dependsOn": [ + "nfs4-handling" + ] + }, + { + "id": "nfs4-tires", + "parent": "nfs4-handling", + "name": "Tire grip and wheel forces", + "purpose": "Per-wheel grip from gravity, slope, surface, weather, front-rear bias, downforce and weight transfer, then the lateral and longitudinal force each wheel delivers, with handbrake and braking variants", + "paths": [ + "src/Physics/NFS4VehiclePhysics.cpp", + "src/Physics/NFS4VehiclePhysics.h" + ], + "functions": [ + { + "name": "TireFactor", + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "lines": [ + 338, + 357 + ] + }, + { + "name": "WheelSurfaceGripFactor", + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "lines": [ + 359, + 372 + ] + }, + { + "name": "WheelBaseRoadGrip", + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "lines": [ + 374, + 381 + ] + }, + { + "name": "WheelDownforceFactor", + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "lines": [ + 393, + 416 + ] + }, + { + "name": "gTransferDamp", + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "lines": [ + 435, + 452 + ] + }, + { + "name": "SteeringAcceleration", + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "lines": [ + 469, + 480 + ] + }, + { + "name": "WheelForce", + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "lines": [ + 566, + 633 + ] + }, + { + "name": "BrakeForce", + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "lines": [ + 635, + 658 + ] + }, + { + "name": "IncrementHandbrakeAccumulator", + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "lines": [ + 511, + 525 + ] + }, + { + "name": "HandbrakeScalingFunction", + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "lines": [ + 527, + 535 + ] + }, + { + "name": "HandbrakeLossOfGrip", + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "lines": [ + 537, + 564 + ] + }, + { + "name": "IsWheelSlipping", + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "lines": [ + 132, + 166 + ] + } + ], + "dependsOn": [ + "nfs4-handling" + ] + }, + { + "id": "nfs4-stabilizers", + "parent": "nfs4-handling", + "name": "Chassis stabilizers and air time", + "purpose": "Velocity and yaw dampers that keep the car planted: lateral slide damping, turning-circle yaw limit, near-stop and neutral decay, stationary burnout lock, airborne drag, spin cap and downforce", + "paths": [ + "src/Physics/NFS4VehiclePhysics.cpp" + ], + "functions": [ + { + "name": "TurningCircle", + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "lines": [ + 790, + 854 + ] + }, + { + "name": "DampLateralVelocity", + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "lines": [ + 875, + 899 + ] + }, + { + "name": "ApplyNeutralGearDeceleration", + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "lines": [ + 912, + 930 + ] + }, + { + "name": "ApplyNearStopDeceleration", + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "lines": [ + 941, + 965 + ] + }, + { + "name": "PreventMovingSideways", + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "lines": [ + 967, + 980 + ] + }, + { + "name": "AirborneDrag", + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "lines": [ + 982, + 998 + ] + }, + { + "name": "LimitAngularVelocity", + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "lines": [ + 1013, + 1023 + ] + }, + { + "name": "WentAirborne", + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "lines": [ + 1078, + 1080 + ] + }, + { + "name": "GoAirborne", + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "lines": [ + 1082, + 1095 + ] + }, + { + "name": "ApplyDownforce", + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "lines": [ + 1097, + 1109 + ] + } + ], + "dependsOn": [ + "nfs4-handling" + ] + }, + { + "id": "chase-camera", + "name": "Follow camera", + "purpose": "Third-person camera on a sphere around the car, aimed along the body heading, with scroll zoom, mouse pitch and mouse orbit, plus a spinning showroom mode", + "paths": [ + "src/Camera/CarCamera.cpp", + "src/Camera/CarCamera.h" + ], + "functions": [ + { + "name": "FollowCar", + "path": "src/Camera/CarCamera.cpp", + "lines": [ + 17, + 30 + ] + }, + { + "name": "_CalculateCameraPosition", + "path": "src/Camera/CarCamera.cpp", + "lines": [ + 32, + 39 + ] + }, + { + "name": "_CalculateZoom", + "path": "src/Camera/CarCamera.cpp", + "lines": [ + 41, + 47 + ] + }, + { + "name": "_CalculatePitch", + "path": "src/Camera/CarCamera.cpp", + "lines": [ + 49, + 54 + ] + }, + { + "name": "_CalculateAngleAroundCar", + "path": "src/Camera/CarCamera.cpp", + "lines": [ + 56, + 78 + ] + } + ], + "dependsOn": [ + "physics-world" + ] + }, + { + "id": "racer-ai", + "name": "Opponent driver", + "purpose": "Rule-based opponent that follows the virtual road with a speed-scaled lookahead, steers through a damper, dodges walls with rangefinder rays and recovers when stuck. Original to this engine, not the shipped AI", + "paths": [ + "src/Race/Agents/RacerAgent.cpp", + "src/Race/Agents/RacerAgent.h", + "src/Physics/Car.cpp", + "src/Physics/Car.h" + ], + "functions": [ + { + "name": "Simulate", + "path": "src/Race/Agents/RacerAgent.cpp", + "lines": [ + 18, + 96 + ] + }, + { + "name": "_CarSpeedToLookahead", + "path": "src/Race/Agents/RacerAgent.cpp", + "lines": [ + 102, + 106 + ] + }, + { + "name": "_CarSpeedToSteeringDamper", + "path": "src/Race/Agents/RacerAgent.cpp", + "lines": [ + 108, + 113 + ] + }, + { + "name": "_UsePrimitiveAI", + "path": "src/Race/Agents/RacerAgent.cpp", + "lines": [ + 115, + 174 + ] + }, + { + "name": "_GenRaycasts", + "path": "src/Physics/Car.cpp", + "lines": [ + 384, + 437 + ] + } + ], + "dependsOn": [ + "physics-world" + ] + } + ], + "cards": [ + { + "id": "physics-stepping", + "node": "physics-world", + "name": "Simulation stepping and gravity", + "summary": "Substepped rigid-body world under standard gravity, with the handling model run once per substep", + "behavior": "The world pulls every body down at 9.81 meters per second squared. Each render frame first copies the chassis transform to the meshes, applies driver input and casts the sensor rays, then advances the solver by the frame time with up to 100 fixed substeps. A hook that fires before each substep runs the selected handling model with the substep length, so the recreated handling runs at the solver rate and not at the render rate. The handling code expresses its rates per 1/32 second tick of the original game and multiplies them by 32 times the substep length. Chassis bodies have zero built-in damping and never sleep.", + "constants": [ + { + "name": "gravity", + "value": -9.81, + "unit": "meters per second squared, continuous", + "source": "src/Physics/PhysicsManager.cpp:44" + }, + { + "name": "maxSubstepsPerFrame", + "value": 100, + "unit": "substeps per render frame", + "source": "src/Physics/PhysicsManager.cpp:84" + }, + { + "name": "originalTickRate", + "value": 32, + "unit": "ticks per second (Hz)", + "source": "src/Physics/NFS4VehiclePhysics.h:267" + } + ], + "sources": [ + { + "path": "src/Physics/PhysicsManager.cpp", + "line": 44 + }, + { + "path": "src/Physics/PhysicsManager.cpp", + "line": 72 + }, + { + "path": "src/Physics/PhysicsManager.cpp", + "line": 84 + }, + { + "path": "src/Physics/Car.cpp", + "line": 61 + }, + { + "path": "src/Physics/Car.cpp", + "line": 378 + } + ], + "capabilities": [ + "fixed substeps", + "pre-tick hook", + "gravity" + ], + "tags": [ + "physics", + "timestep", + "gravity" + ] + }, + { + "id": "arcade-drive", + "node": "physics-world", + "name": "Simple raycast-vehicle drive", + "summary": "Fallback drive model with rear-wheel engine force up to a speed cap and keyboard steering that ramps toward a clamp", + "behavior": "In the plain Bullet model the throttle sets a fixed engine force on both rear wheels only while speed sits under the car top speed, and cuts it at or above that speed. Reverse applies the same force backward. Braking applies a fixed brake force to the rear wheels. Steering keys add a per-frame increment to the wheel angle up to a clamp, and the angle walks back toward center by the same increment when no key is held. Opponents always use this model with the top speed forced to 100 kilometers per hour. Each wheel has a suspension spring, damper, friction slip and roll influence copied from the car data.", + "constants": [ + { + "name": "opponentTopSpeed", + "value": 100, + "unit": "kilometers per hour", + "source": "src/Race/Agents/RacerAgent.cpp:15" + } + ], + "sources": [ + { + "path": "src/Physics/Car.cpp", + "line": 74 + }, + { + "path": "src/Physics/Car.cpp", + "line": 75 + }, + { + "path": "src/Physics/Car.cpp", + "line": 261 + }, + { + "path": "src/Physics/Car.cpp", + "line": 283 + }, + { + "path": "src/Physics/PhysicsManager.cpp", + "line": 147 + } + ], + "capabilities": [ + "engine force", + "speed cap", + "steering ramp", + "suspension" + ], + "tags": [ + "movement", + "acceleration", + "steering", + "ai" + ] + }, + { + "id": "input-ramps", + "node": "nfs4-handling", + "name": "Pedal and steering ramps", + "summary": "Throttle, brake and steering move toward their targets at a fixed rate per tick instead of jumping", + "behavior": "Steering lives on a range of plus or minus 128. Each tick it moves toward the target at the turn-in rate, or at the faster turn-out rate when it moves back toward center, and snaps to the target once the remaining gap is smaller than one step. Throttle rises or falls by 16 out of 255 per tick. Brake pressure uses an eight-entry curve indexed by the current pressure, with one curve for pressing and another for releasing. Default car data turns in at 16 and turns out at 32 steering units per tick.", + "constants": [ + { + "name": "steeringRange", + "value": 128, + "unit": "steering units", + "source": "src/Physics/NFS4VehiclePhysics.cpp:690" + }, + { + "name": "throttleStep", + "value": 16, + "unit": "throttle units out of 255 per original 32 Hz tick, scaled by the real substep length", + "source": "src/Physics/NFS4VehiclePhysics.cpp:712" + }, + { + "name": "defaultTurnInRate", + "value": 16, + "unit": "steering units per original 32 Hz tick, scaled by the real substep length", + "source": "src/Physics/NFS4VehiclePhysics.h:67" + }, + { + "name": "defaultTurnOutRate", + "value": 32, + "unit": "steering units per original 32 Hz tick, scaled by the real substep length", + "source": "src/Physics/NFS4VehiclePhysics.h:68" + } + ], + "sources": [ + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 694 + }, + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 701 + }, + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 713 + }, + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 724 + } + ], + "capabilities": [ + "input smoothing", + "steering return", + "brake curve" + ], + "tags": [ + "steering", + "throttle", + "input", + "feel" + ] + }, + { + "id": "speed-sensitive-steering", + "node": "nfs4-handling", + "name": "Speed-sensitive steering angle", + "summary": "Wheel angle shrinks at high speed and while the car already slides in the steered direction", + "behavior": "Steering is zero while airborne. On the ground the steering value is multiplied by a slip factor: when the car already slides the same way it steers, the factor becomes twice the slip ratio, capped at 1, once forward speed passes 26.7 meters per second with slip above 0.05, or passes 13.4 with slip above 0.15. With the handbrake held and counter-steer applied, the factor is 0.85 minus 0.0056 per meter per second, floored at 0.55. Above 40 meters per second the value is also divided by 0.015 times speed, held between 1 and 1.5. The result is scaled by the car minimum steering acceleration and clamped to plus or minus one turn fraction.", + "constants": [ + { + "name": "highSpeedSlipGate", + "value": 26.7, + "unit": "meters per second", + "source": "src/Physics/NFS4VehiclePhysics.cpp:258" + }, + { + "name": "mediumSpeedSlipGate", + "value": 13.4, + "unit": "meters per second", + "source": "src/Physics/NFS4VehiclePhysics.cpp:257" + }, + { + "name": "counterSteerBase", + "value": 0.85, + "unit": "multiplier", + "source": "src/Physics/NFS4VehiclePhysics.cpp:281" + }, + { + "name": "counterSteerFloor", + "value": 0.55, + "unit": "multiplier", + "source": "src/Physics/NFS4VehiclePhysics.cpp:282" + }, + { + "name": "steeringFalloffSpeed", + "value": 40, + "unit": "meters per second", + "source": "src/Physics/NFS4VehiclePhysics.cpp:300" + }, + { + "name": "steeringFalloffRate", + "value": 0.015, + "unit": "divisor per meter per second", + "source": "src/Physics/NFS4VehiclePhysics.cpp:301" + } + ], + "sources": [ + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 256 + }, + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 270 + }, + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 288 + }, + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 302 + } + ], + "capabilities": [ + "speed-sensitive steering", + "slip assist", + "counter-steer" + ], + "tags": [ + "steering", + "drift", + "handling" + ] + }, + { + "id": "torque-and-gearing", + "node": "nfs4-powertrain", + "name": "Torque curve and gear force", + "summary": "Engine torque read from a 21-point curve, multiplied by gear ratio and efficiency over mass, then clamped per gear", + "behavior": "The torque curve holds one sample every 500 RPM and the engine speed is linearly interpolated between neighbors. The force for the current gear is torque times the gear velocity-to-RPM ratio times the gear efficiency, divided by ten times the car mass. First gear caps that drive acceleration at 10, reverse floors it at minus 6 and every other gear caps it at 8. Default car data weighs 1750 kilograms and revs from 1000 to a 7000 RPM redline.", + "constants": [ + { + "name": "torqueSampleSpacing", + "value": 500, + "unit": "RPM", + "source": "src/Physics/NFS4VehiclePhysics.cpp:170" + }, + { + "name": "firstGearForceCap", + "value": 10, + "unit": "internal acceleration units per tick, scale unverified", + "source": "src/Physics/NFS4VehiclePhysics.cpp:191" + }, + { + "name": "reverseForceFloor", + "value": -6, + "unit": "internal acceleration units per tick, scale unverified", + "source": "src/Physics/NFS4VehiclePhysics.cpp:193" + }, + { + "name": "otherGearForceCap", + "value": 8, + "unit": "internal acceleration units per tick, scale unverified", + "source": "src/Physics/NFS4VehiclePhysics.cpp:195" + }, + { + "name": "defaultMass", + "value": 1750, + "unit": "kilograms", + "source": "src/Physics/NFS4VehiclePhysics.h:53" + }, + { + "name": "defaultRedline", + "value": 7000, + "unit": "RPM", + "source": "src/Physics/NFS4VehiclePhysics.h:56" + } + ], + "sources": [ + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 169 + }, + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 181 + }, + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 189 + }, + { + "path": "src/Physics/NFS4VehiclePhysics.h", + "line": 45 + } + ], + "capabilities": [ + "torque curve", + "gear ratios", + "force clamp" + ], + "tags": [ + "engine", + "acceleration", + "gears" + ] + }, + { + "id": "drag-top-speed", + "node": "nfs4-powertrain", + "name": "Cubic drag and top speed", + "summary": "Drag grows with the cube of forward speed and is calibrated so the top gear tops out at the car rated speed", + "behavior": "A drag coefficient is derived from the torque available in top gear at the rated top speed, scaled by 1.5, 0.98 and divided by the cube of that speed times 0.36757159. Drag is that coefficient times 0.2450477 times the cube of forward speed. Past the rated speed an extra 0.01 times the square of the excess is added, which acts as a soft limiter. On the ground drag is subtracted from drive force when moving forward and added twice when rolling backward. In the air the car loses half the drag going forward. Default rated speed is 80 meters per second.", + "constants": [ + { + "name": "dragScale", + "value": 0.2450477, + "unit": "multiplier", + "source": "src/Physics/NFS4VehiclePhysics.cpp:216" + }, + { + "name": "dragCalibration", + "value": 0.36757159, + "unit": "divisor", + "source": "src/Physics/NFS4VehiclePhysics.cpp:203" + }, + { + "name": "overSpeedPenalty", + "value": 0.01, + "unit": "per meter per second squared", + "source": "src/Physics/NFS4VehiclePhysics.cpp:224" + }, + { + "name": "defaultRatedSpeed", + "value": 80, + "unit": "meters per second", + "source": "src/Physics/NFS4VehiclePhysics.h:54" + } + ], + "sources": [ + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 200 + }, + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 211 + }, + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 221 + }, + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 1475 + } + ], + "capabilities": [ + "aerodynamic drag", + "speed limiter" + ], + "tags": [ + "speed", + "drag", + "top speed" + ] + }, + { + "id": "engine-traction-states", + "node": "nfs4-powertrain", + "name": "Engine speed and traction states", + "summary": "Engine RPM chases a throttle target and decides each tick whether the car accelerates, cruises, engine-brakes or loses grip", + "behavior": "Target RPM is redline times throttle, clamped to the idle and redline range. In gear and on the ground, a 500 RPM deadband around the wheel-derived RPM counts as cruising unless the target sits within 300 of redline, and cruising force equals drag. When the engine leads the wheels by more than a sixth of redline in the lower gears, or the car moves against the gear, RPM drops by 125 or by a per-gear step. Lifting the throttle turns drive into engine braking scaled by the gas-off factor. Coasting more than 1.15 times the gear speed ceiling flags lost grip. Accelerating force grows with slip by 0.6 per unit, up to 1.5 times. RPM above four thirds of redline also flags lost grip.", + "constants": [ + { + "name": "cruiseDeadband", + "value": 500, + "unit": "RPM", + "source": "src/Physics/NFS4VehiclePhysics.cpp:1320" + }, + { + "name": "overRevDivisor", + "value": 6, + "unit": "fraction of redline", + "source": "src/Physics/NFS4VehiclePhysics.cpp:1328" + }, + { + "name": "overRevDrop", + "value": 125, + "unit": "RPM per tick", + "source": "src/Physics/NFS4VehiclePhysics.cpp:1342" + }, + { + "name": "gearOverSpeedMargin", + "value": 1.15, + "unit": "multiplier on gear speed ceiling", + "source": "src/Physics/NFS4VehiclePhysics.cpp:1374" + }, + { + "name": "slipForceGain", + "value": 0.6, + "unit": "multiplier per unit slip ratio", + "source": "src/Physics/NFS4VehiclePhysics.cpp:1410" + }, + { + "name": "slipForceCap", + "value": 1.5, + "unit": "multiplier", + "source": "src/Physics/NFS4VehiclePhysics.cpp:1411" + }, + { + "name": "defaultGasOffFactor", + "value": 0.4, + "unit": "multiplier", + "source": "src/Physics/NFS4VehiclePhysics.h:57" + } + ], + "sources": [ + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 1293 + }, + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 1287 + }, + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 1350 + }, + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 1389 + }, + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 1396 + } + ], + "capabilities": [ + "engine braking", + "rpm model", + "wheelspin state" + ], + "tags": [ + "engine", + "acceleration", + "traction" + ] + }, + { + "id": "gear-shifting", + "node": "nfs4-powertrain", + "name": "Gear shift delay and rev blips", + "summary": "A shift cuts drive for a fixed number of ticks while the engine free-revs, with a rev step on upshift and a blip on downshift", + "behavior": "Gear keys step the requested gear between reverse and sixth. A new gear starts a counter equal to the car shift delay, six ticks by default, and drive force is zero until it reaches zero. While out of gear or airborne, RPM rises by 250 per tick toward the target or falls by 150 once above it. During an upshift the code subtracts a negative step, so RPM actually climbs by 50 per tick, or by 75 when the shift delay is under five ticks, doubled above redline, although the stated intent is a drop. During a downshift with throttle the engine gains a per-gear blip. Below the first-gear speed ceiling the gearbox forces anything above third down to third.", + "constants": [ + { + "name": "defaultShiftDelay", + "value": 6, + "unit": "ticks per original 32 Hz tick, scaled by the real substep length", + "source": "src/Physics/NFS4VehiclePhysics.h:70" + }, + { + "name": "freeRevRise", + "value": 250, + "unit": "RPM per tick", + "source": "src/Physics/NFS4VehiclePhysics.cpp:1437" + }, + { + "name": "freeRevFall", + "value": 150, + "unit": "RPM per tick", + "source": "src/Physics/NFS4VehiclePhysics.cpp:1444" + }, + { + "name": "upshiftDrop", + "value": -50, + "unit": "RPM per tick", + "source": "src/Physics/NFS4VehiclePhysics.cpp:1449" + }, + { + "name": "quickUpshiftDrop", + "value": -75, + "unit": "RPM per tick", + "source": "src/Physics/NFS4VehiclePhysics.cpp:1451" + } + ], + "sources": [ + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 743 + }, + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 1296 + }, + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 1308 + }, + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 1436 + }, + { + "path": "src/Physics/Car.cpp", + "line": 104 + } + ], + "capabilities": [ + "manual gears", + "shift delay", + "rev blip" + ], + "tags": [ + "gears", + "engine", + "transmission" + ] + }, + { + "id": "tire-grip", + "node": "nfs4-tires", + "name": "Wheel grip budget", + "summary": "Each wheel grip comes from local gravity, slope, surface and weather, split front to rear and scaled by downforce and weight transfer", + "behavior": "The base grip is the downward part of gravity in car space times the lateral grip multiplier, 3.5 by default, times a surface factor. The surface factor uses the car tilt clamped to at least 0.75 and the mean of 1 and a 24-entry road table, reduced to 0.9 in rain and 0.8 in snow. Front wheels get the front bias share, 0.5 by default, and rear wheels the rest. The front share loses the damped weight transfer and the rear share gains it. On a dry road a tire factor of one twentieth at the default tire setting, and never above one sixteenth, sets the margin between grip and force before a wheel saturates.", + "constants": [ + { + "name": "localGravity", + "value": -9.81, + "unit": "meters per second squared", + "source": "src/Physics/NFS4VehiclePhysics.cpp:376" + }, + { + "name": "slopeFloor", + "value": 0.75, + "unit": "cosine of tilt", + "source": "src/Physics/NFS4VehiclePhysics.cpp:361" + }, + { + "name": "rainGrip", + "value": 0.9, + "unit": "multiplier", + "source": "src/Physics/NFS4VehiclePhysics.cpp:366" + }, + { + "name": "snowGrip", + "value": 0.8, + "unit": "multiplier", + "source": "src/Physics/NFS4VehiclePhysics.cpp:368" + }, + { + "name": "defaultLateralGrip", + "value": 3.5, + "unit": "multiplier", + "source": "src/Physics/NFS4VehiclePhysics.h:58" + }, + { + "name": "defaultFrontGripShare", + "value": 0.5, + "unit": "fraction", + "source": "src/Physics/NFS4VehiclePhysics.h:59" + }, + { + "name": "dryTireFloor", + "value": 16, + "unit": "divisor", + "source": "src/Physics/NFS4VehiclePhysics.cpp:345" + } + ], + "sources": [ + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 359 + }, + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 380 + }, + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 387 + }, + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 418 + }, + { + "path": "src/Physics/NFS4VehiclePhysics.h", + "line": 270 + } + ], + "capabilities": [ + "grip model", + "surface types", + "weather grip" + ], + "tags": [ + "grip", + "tires", + "handling", + "weather" + ] + }, + { + "id": "downforce-weight-transfer", + "node": "nfs4-tires", + "name": "Downforce and weight transfer", + "summary": "Speed-scaled downforce adds grip, more at the rear and more again when a spoiler car brakes, while acceleration shifts grip between axles", + "behavior": "Below 10 meters per second each wheel downforce multiplier is one plus the car downforce coefficient, 0.002 by default. Above that, the coefficient is scaled by forward speed and the rear axle takes 1.5 times as much, or 1.75 times when a spoiler car brakes at 0.15 or more. A separate pass pushes the chassis down with an acceleration of the coefficient times speed times 32. Weight transfer is the summed longitudinal wheel force times the transfer factor, 0.5 by default, carried to the next tick. Before use it is scaled by 0.75 when negative, by 0.5 in one flagged case when positive, and slightly in wet weather.", + "constants": [ + { + "name": "downforceSpeedGate", + "value": 10, + "unit": "meters per second", + "source": "src/Physics/NFS4VehiclePhysics.cpp:394" + }, + { + "name": "rearDownforceShare", + "value": 1.5, + "unit": "multiplier", + "source": "src/Physics/NFS4VehiclePhysics.cpp:403" + }, + { + "name": "spoilerBrakeDownforce", + "value": 1.75, + "unit": "multiplier", + "source": "src/Physics/NFS4VehiclePhysics.cpp:406" + }, + { + "name": "defaultDownforceCoefficient", + "value": 0.002, + "unit": "per meter per second", + "source": "src/Physics/NFS4VehiclePhysics.h:65" + }, + { + "name": "defaultWeightTransfer", + "value": 0.5, + "unit": "multiplier", + "source": "src/Physics/NFS4VehiclePhysics.h:66" + }, + { + "name": "negativeTransferDamp", + "value": 0.75, + "unit": "multiplier", + "source": "src/Physics/NFS4VehiclePhysics.cpp:439" + } + ], + "sources": [ + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 393 + }, + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 1103 + }, + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 1188 + }, + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 435 + } + ], + "capabilities": [ + "downforce", + "weight transfer", + "spoiler" + ], + "tags": [ + "grip", + "aero", + "physics feel" + ] + }, + { + "id": "wheel-forces-understeer", + "node": "nfs4-tires", + "name": "Cornering force and understeer", + "summary": "Each wheel pushes against its own sideways slip, front force softened and rear force stiffened by the understeer gradient", + "behavior": "Each wheel reads a planar vector of minus half the local velocity per tick, nudged by a yaw-rate term, rotated into the steered wheel frame. Its cornering push is the sine of half the slip angle times the grip margin times 4, or times 2 in reverse, capped at twice the minimum steering acceleration, 18 by default. Below 13.4 meters per second with almost no throttle that push drops to a quarter. Fronts scale it by 0.8 times the understeer gradient and rears by 1.4 divided by it. Wheel forces are halved, scaled by the slope, summed into a velocity change, and front minus rear lateral force makes the yaw torque. In first gear near redline on grippy asphalt the rear wheels lose half their drive to wheelspin.", + "constants": [ + { + "name": "frontCorneringShare", + "value": 0.8, + "unit": "multiplier", + "source": "src/Physics/NFS4VehiclePhysics.cpp:620" + }, + { + "name": "rearCorneringShare", + "value": 1.4, + "unit": "multiplier", + "source": "src/Physics/NFS4VehiclePhysics.cpp:622" + }, + { + "name": "lowSpeedSteerCut", + "value": 0.25, + "unit": "multiplier", + "source": "src/Physics/NFS4VehiclePhysics.cpp:610" + }, + { + "name": "slipGain", + "value": 4, + "unit": "multiplier", + "source": "src/Physics/NFS4VehiclePhysics.cpp:477" + }, + { + "name": "defaultMinSteeringAccel", + "value": 18, + "unit": "internal acceleration units per tick, scale unverified", + "source": "src/Physics/NFS4VehiclePhysics.h:63" + }, + { + "name": "wheelForceShare", + "value": 0.5, + "unit": "multiplier", + "source": "src/Physics/NFS4VehiclePhysics.cpp:1174" + } + ], + "sources": [ + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 457 + }, + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 566 + }, + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 604 + }, + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 1181 + }, + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 502 + } + ], + "capabilities": [ + "tire forces", + "understeer", + "yaw torque", + "wheelspin" + ], + "tags": [ + "handling", + "cornering", + "steering", + "physics feel" + ] + }, + { + "id": "handbrake-slide", + "node": "nfs4-tires", + "name": "Handbrake slide", + "summary": "Holding the handbrake builds a counter that bleeds rear lateral grip along an S-curve until the rear steps out", + "behavior": "While the handbrake is held or grip is lost, a counter grows by 1 per tick on a dry road, 3 in rain and 4 in snow, up to 384, and resets on release. The rear wheels, and any front wheel whose braking exceeds its grip, then use a sliding force instead of the normal tire force, unless the car moves slower than 2.2351501 meters per second. The sliding force keeps three quarters of the lateral push, or the ratio of grip to speed when speed is higher, times an S-curve of the counter over 384 that falls from about 1 to about 0.2 as the counter fills, times 0.75. Neutral cuts both components to 0.05. The handbrake also moves 0.05 of brake balance to the front.", + "constants": [ + { + "name": "dryCounterStep", + "value": 1, + "unit": "counts per tick", + "source": "src/Physics/NFS4VehiclePhysics.cpp:515" + }, + { + "name": "rainCounterStep", + "value": 3, + "unit": "counts per tick", + "source": "src/Physics/NFS4VehiclePhysics.cpp:518" + }, + { + "name": "snowCounterStep", + "value": 4, + "unit": "counts per tick", + "source": "src/Physics/NFS4VehiclePhysics.cpp:521" + }, + { + "name": "counterCap", + "value": 384, + "unit": "counts", + "source": "src/Physics/NFS4VehiclePhysics.cpp:524" + }, + { + "name": "slideMinimumSpeed", + "value": 2.2351501, + "unit": "meters per second", + "source": "src/Physics/NFS4VehiclePhysics.cpp:596" + }, + { + "name": "slideCurveSteepness", + "value": -12.56636, + "unit": "exponent scale", + "source": "src/Physics/NFS4VehiclePhysics.cpp:530" + }, + { + "name": "handbrakeFrontBrakeShift", + "value": 0.05, + "unit": "fraction of brake balance", + "source": "src/Physics/NFS4VehiclePhysics.cpp:648" + } + ], + "sources": [ + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 537 + }, + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 552 + }, + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 595 + }, + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 1238 + } + ], + "capabilities": [ + "handbrake", + "drift", + "oversteer" + ], + "tags": [ + "drift", + "handbrake", + "handling" + ] + }, + { + "id": "braking", + "node": "nfs4-tires", + "name": "Brake force and balance", + "summary": "Brake pressure times the car maximum deceleration, capped so it cannot reverse the car in one tick, split by brake bias", + "behavior": "Brake deceleration is pressure times the maximum braking deceleration, 10 by default, capped at 32 times forward speed so a single tick never pushes the car backward. A spoiler car braking past 0.15 adds speed times the downforce coefficient. Front wheels take the front bias, 0.6 by default, and rears the rest. The result opposes the direction of travel and is subtracted from each wheel drive share, which follows the front-drive ratio. With antilock brakes the longitudinal force is clamped to the wheel grip.", + "constants": [ + { + "name": "defaultMaxBrakeDecel", + "value": 10, + "unit": "meters per second squared", + "source": "src/Physics/NFS4VehiclePhysics.h:69" + }, + { + "name": "brakeCapPerSpeed", + "value": 32, + "unit": "ticks per second", + "source": "src/Physics/NFS4VehiclePhysics.cpp:638" + }, + { + "name": "spoilerBrakeGate", + "value": 0.15, + "unit": "brake pressure fraction", + "source": "src/Physics/NFS4VehiclePhysics.cpp:642" + }, + { + "name": "defaultFrontBrakeBias", + "value": 0.6, + "unit": "fraction", + "source": "src/Physics/NFS4VehiclePhysics.h:61" + } + ], + "sources": [ + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 635 + }, + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 651 + }, + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 660 + }, + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 599 + } + ], + "capabilities": [ + "brake bias", + "abs", + "brake cap" + ], + "tags": [ + "braking", + "handling" + ] + }, + { + "id": "chassis-stabilizers", + "node": "nfs4-stabilizers", + "name": "Slide and yaw dampers", + "summary": "Per-tick multipliers bleed sideways speed and yaw so slides settle and the car stops cleanly", + "behavior": "Sideways speed keeps 0.9 of itself per tick below 1 meter per second, 0.99 above 2, and a linear blend between. The turning-circle pass compares yaw rate times the turning radius, 11 meters by default, against the car speed. When yaw outruns that, it scales yaw by speed over that product, capped at 0.98. Otherwise it keeps yaw at 0.95, or 0.99 while sliding. With throttle and brake both low and the car under 5 meters per second, or 4 in reverse, velocity and spin keep 0.8 per tick. Neutral coasting keeps 0.998 per tick, or 0.99 when slow or steering hard. Half throttle or more with heavy brake, hard steer and low speed zeroes velocity, a stationary burnout.", + "constants": [ + { + "name": "lowLateralKeep", + "value": 0.9, + "unit": "multiplier per original 32 Hz tick, scaled by the real substep length", + "source": "src/Physics/NFS4VehiclePhysics.cpp:876" + }, + { + "name": "highLateralKeep", + "value": 0.99, + "unit": "multiplier per original 32 Hz tick, scaled by the real substep length", + "source": "src/Physics/NFS4VehiclePhysics.cpp:878" + }, + { + "name": "yawSpeedRatioCap", + "value": 0.98, + "unit": "multiplier", + "source": "src/Physics/NFS4VehiclePhysics.cpp:824" + }, + { + "name": "defaultTurningRadius", + "value": 11, + "unit": "meters", + "source": "src/Physics/NFS4VehiclePhysics.h:64" + }, + { + "name": "nearStopKeep", + "value": 0.8, + "unit": "multiplier per original 32 Hz tick, scaled by the real substep length", + "source": "src/Physics/NFS4VehiclePhysics.cpp:950" + }, + { + "name": "nearStopSpeed", + "value": 5, + "unit": "meters per second", + "source": "src/Physics/NFS4VehiclePhysics.cpp:949" + }, + { + "name": "neutralCoastKeep", + "value": 0.998, + "unit": "multiplier per original 32 Hz tick, scaled by the real substep length", + "source": "src/Physics/NFS4VehiclePhysics.cpp:921" + } + ], + "sources": [ + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 884 + }, + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 837 + }, + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 955 + }, + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 973 + }, + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 1205 + } + ], + "capabilities": [ + "lateral damping", + "yaw limit", + "stop assist", + "burnout" + ], + "tags": [ + "physics feel", + "stability", + "handling" + ] + }, + { + "id": "airborne", + "node": "nfs4-stabilizers", + "name": "Air time", + "summary": "The car counts as airborne once suspension reaches 0.6, then keeps its pitch steady, takes quadratic drag and caps its spin", + "behavior": "Ground contact comes from the wheel rays, and the car counts as off the ground when the shortest suspension length is 0.6 or more. On the tick it lifts off while moving upward along its own up axis, pitch rate keeps only 0.15 of itself, so ramps do not flip the car. In the air drag is quadratic per world axis, with coefficients 0.006, 0.004 and 0.002 blended by orientation, and every axis of spin is clamped to 2.4 radians per second. Wheel forces and steering are off in the air, and the engine free-revs toward the throttle target plus the first-gear blip.", + "constants": [ + { + "name": "airborneSuspensionLength", + "value": 0.6, + "unit": "meters", + "source": "src/Physics/NFS4VehiclePhysics.cpp:1079" + }, + { + "name": "liftoffPitchKeep", + "value": 0.15, + "unit": "multiplier", + "source": "src/Physics/NFS4VehiclePhysics.cpp:1089" + }, + { + "name": "sideAirDrag", + "value": 0.006, + "unit": "per meter, applied each substep", + "source": "src/Physics/NFS4VehiclePhysics.cpp:983" + }, + { + "name": "spinCap", + "value": 2.4, + "unit": "radians per second", + "source": "src/Physics/NFS4VehiclePhysics.cpp:1014" + } + ], + "sources": [ + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 1082 + }, + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 1000 + }, + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 1230 + }, + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 1132 + }, + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 1431 + } + ], + "capabilities": [ + "jumps", + "air drag", + "spin cap" + ], + "tags": [ + "jump", + "airborne", + "physics" + ] + }, + { + "id": "skid-detection", + "node": "nfs4-tires", + "name": "Wheel slip for skid marks", + "summary": "Rules that mark a wheel as slipping so skid marks and effects can follow the handling state", + "behavior": "A grounded wheel slips when the handbrake locks it as a rear wheel, when lost grip affects a driven wheel, or on heavy braking. Heavy braking means pressure above 0.8 at more than 5 meters per second. A driven wheel in reverse, first or second also slips at more than half throttle when the engine speed implies a road speed more than 5 meters per second faster than the car really moves. A wheel counts as driven when the front-drive ratio gives its axle more than one percent of the drive.", + "constants": [ + { + "name": "burnoutThrottle", + "value": 0.5, + "unit": "throttle fraction", + "source": "src/Physics/NFS4VehiclePhysics.cpp:150" + }, + { + "name": "burnoutSpeedGap", + "value": 5, + "unit": "meters per second", + "source": "src/Physics/NFS4VehiclePhysics.cpp:156" + }, + { + "name": "lockupBrake", + "value": 0.8, + "unit": "brake pressure fraction", + "source": "src/Physics/NFS4VehiclePhysics.cpp:162" + } + ], + "sources": [ + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 132 + }, + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 140 + }, + { + "path": "src/Physics/NFS4VehiclePhysics.cpp", + "line": 149 + }, + { + "path": "src/Race/SkidMarkSystem.cpp", + "line": 81 + } + ], + "capabilities": [ + "skid marks", + "wheelspin", + "lockup" + ], + "tags": [ + "effects", + "tires", + "drift" + ] + }, + { + "id": "follow-camera", + "node": "chase-camera", + "name": "Orbit follow camera", + "summary": "Camera sits a fixed distance behind the car heading at a set pitch, with mouse orbit and scroll zoom", + "behavior": "The camera keeps no lag or spring: every frame it is placed on a sphere around the car body. Horizontal offset is distance times the cosine of pitch, and height is distance times the sine of pitch above the body. The offset points opposite the body heading plus a user orbit angle, so the view swings rigidly with the car. Defaults are 10 units of distance and 10 degrees of pitch. Each scroll step changes distance by 0.1, right-drag changes pitch by 0.1 degree per pixel, and left-drag orbits by 0.3 degree per pixel. A showroom mode spins the orbit at an angular velocity, 20 degrees per second by default, steered by drag input and limited to 5 to 20 units of zoom.", + "constants": [ + { + "name": "defaultDistance", + "value": 10, + "unit": "world units", + "source": "src/Camera/CarCamera.h:23" + }, + { + "name": "defaultPitch", + "value": 10, + "unit": "degrees", + "source": "src/Camera/CarCamera.h:25" + }, + { + "name": "zoomStep", + "value": ".1f", + "unit": "world units per scroll step", + "source": "src/Camera/CarCamera.cpp:42" + }, + { + "name": "orbitRate", + "value": 0.3, + "unit": "degrees per mouse pixel", + "source": "src/Camera/CarCamera.cpp:74" + }, + { + "name": "showroomSpin", + "value": 20, + "unit": "degrees per second", + "source": "src/Camera/CarCamera.h:12" + }, + { + "name": "showroomMaxZoom", + "value": 20, + "unit": "world units", + "source": "src/Camera/CarCamera.h:34" + } + ], + "sources": [ + { + "path": "src/Camera/CarCamera.cpp", + "line": 17 + }, + { + "path": "src/Camera/CarCamera.cpp", + "line": 33 + }, + { + "path": "src/Camera/CarCamera.cpp", + "line": 81 + }, + { + "path": "src/Camera/CarCamera.cpp", + "line": 85 + } + ], + "capabilities": [ + "third-person camera", + "orbit", + "zoom" + ], + "tags": [ + "camera" + ] + }, + { + "id": "ai-road-following", + "node": "racer-ai", + "name": "Opponent road following", + "summary": "Opponents aim at a virtual-road point further ahead at higher speed and soften steering as they go faster", + "behavior": "Each tick the opponent finds its nearest road block and virtual-road node, then picks a target node 2 nodes ahead plus 4 more for each tenth of top speed. Steering is the signed yaw angle to that point times a damper of 0.6 minus the speed ratio, floored at 0.1. The opponent accelerates only when the damped angle is within 0.15 of the raw angle or speed is at or below 20 kilometers per hour. A forward-diagonal ray shorter than 1 meter overrides steering toward the open side, and a forward ray of 1 meter or less brakes. Note that this AI is original to the engine and does not recreate the shipped opponents.", + "constants": [ + { + "name": "baseLookahead", + "value": 2, + "unit": "road nodes", + "source": "src/Race/Agents/RacerAgent.cpp:104" + }, + { + "name": "lookaheadPerTenth", + "value": 4, + "unit": "road nodes per tenth of top speed", + "source": "src/Race/Agents/RacerAgent.cpp:104" + }, + { + "name": "steeringDamper", + "value": ".6f", + "unit": "multiplier", + "source": "src/Race/Agents/RacerAgent.h:40" + }, + { + "name": "steeringDamperFloor", + "value": 0.1, + "unit": "multiplier", + "source": "src/Race/Agents/RacerAgent.cpp:112" + }, + { + "name": "accelerateAngleTolerance", + "value": 0.15, + "unit": "radians", + "source": "src/Race/Agents/RacerAgent.cpp:160" + }, + { + "name": "minimumSpeed", + "value": 20, + "unit": "kilometers per hour", + "source": "src/Race/Agents/RacerAgent.h:41" + } + ], + "sources": [ + { + "path": "src/Race/Agents/RacerAgent.cpp", + "line": 102 + }, + { + "path": "src/Race/Agents/RacerAgent.cpp", + "line": 108 + }, + { + "path": "src/Race/Agents/RacerAgent.cpp", + "line": 153 + }, + { + "path": "src/Race/Agents/RacerAgent.cpp", + "line": 161 + } + ], + "capabilities": [ + "path following", + "lookahead", + "obstacle avoidance" + ], + "tags": [ + "ai", + "steering", + "racing" + ] + }, + { + "id": "ai-stuck-recovery", + "node": "racer-ai", + "name": "Opponent stuck recovery", + "summary": "A three-state machine reverses a stuck opponent toward open space and resets it to the road when that fails", + "behavior": "An opponent that stays in one road block for more than 200 ticks counts as making no progress. After 60 such ticks it reverses for 60 ticks, up to 4 attempts. While reversing it steers toward the side with more clearance, weighting the forward-diagonal rays 3 times the side rays, scaled to at most half lock. When the sides read about equal it alternates 0.3 left and right per attempt. An upside-down car, a car moving backward through blocks, or a car out of attempts is reset onto the nearest road node with a small random sideways offset.", + "constants": [ + { + "name": "noProgressTicks", + "value": 200, + "unit": "ticks, one per AI update", + "source": "src/Race/Agents/RacerAgent.h:39" + }, + { + "name": "stuckTicks", + "value": 60, + "unit": "ticks, one per AI update", + "source": "src/Race/Agents/RacerAgent.h:42" + }, + { + "name": "reverseTicks", + "value": 60, + "unit": "ticks, one per AI update", + "source": "src/Race/Agents/RacerAgent.h:43" + }, + { + "name": "maxReverseAttempts", + "value": 4, + "unit": "attempts", + "source": "src/Race/Agents/RacerAgent.h:44" + }, + { + "name": "forwardRayWeight", + "value": 3, + "unit": "multiplier", + "source": "src/Race/Agents/RacerAgent.cpp:131" + }, + { + "name": "fallbackReverseSteer", + "value": 0.3, + "unit": "steering fraction", + "source": "src/Race/Agents/RacerAgent.cpp:141" + } + ], + "sources": [ + { + "path": "src/Race/Agents/RacerAgent.cpp", + "line": 18 + }, + { + "path": "src/Race/Agents/RacerAgent.cpp", + "line": 36 + }, + { + "path": "src/Race/Agents/RacerAgent.cpp", + "line": 123 + }, + { + "path": "src/Race/Agents/RacerAgent.cpp", + "line": 140 + } + ], + "capabilities": [ + "state machine", + "stuck recovery", + "respawn" + ], + "tags": [ + "ai", + "recovery" + ] + }, + { + "id": "rangefinders", + "node": "racer-ai", + "name": "Rangefinder sensor fan", + "summary": "Nineteen track-only rays fan across the front half of the car, longer straight ahead, plus up and down rays for flip checks", + "behavior": "Every frame the car casts 19 rays from its body center, starting at 90 degrees to one side and stepping 10 degrees to the other side. The straight-ahead rays reach 5 meters, falling through 3, 2 and 1.5 to 1 meter at the sides. Rays hit only track geometry and pass through other cars. A miss reads as the far value of 5 meters. One ray up and one down, 1 meter each, detect a flipped car and whether it sits over the road. The same readings feed the rule AI and an unfinished neural-network driver.", + "constants": [ + { + "name": "rayCount", + "value": 19, + "unit": "rays", + "source": "src/Physics/Car.h:30" + }, + { + "name": "rayFarValue", + "value": 5, + "unit": "meters", + "source": "src/Physics/Car.h:31" + }, + { + "name": "raySpacing", + "value": 10, + "unit": "degrees", + "source": "src/Physics/Car.h:32" + }, + { + "name": "verticalRayLength", + "value": 1, + "unit": "meters", + "source": "src/Physics/Car.h:33" + }, + { + "name": "forwardRayLength", + "value": 5, + "unit": "meters", + "source": "src/Physics/Car.cpp:10" + } + ], + "sources": [ + { + "path": "src/Physics/Car.cpp", + "line": 394 + }, + { + "path": "src/Physics/Car.cpp", + "line": 397 + }, + { + "path": "src/Physics/Car.cpp", + "line": 404 + }, + { + "path": "src/Physics/Car.cpp", + "line": 427 + } + ], + "capabilities": [ + "raycast sensors", + "flip detection" + ], + "tags": [ + "ai", + "sensors", + "collision" + ] + } + ] +} diff --git a/data/games/rage-racer.json b/data/games/rage-racer.json new file mode 100644 index 0000000..b003906 --- /dev/null +++ b/data/games/rage-racer.json @@ -0,0 +1,1719 @@ +{ + "version": 1, + "id": "rage-racer", + "title": "Rage Racer", + "repo": "https://github.com/khasinski/rage-racer-decomp", + "sha": "fdc6d1ebc9c6520497702847271738b8331bb256", + "nodes": [ + { + "id": "player-car", + "name": "Player car update", + "purpose": "Top-level per-update pass for the player vehicle: reads pad input, runs the drivetrain, resolves track boundaries from the four body corners, applies wall pushes, collides with the field and drives the body pitch and roll effects", + "paths": [ + "src/main/PAL/main/car/update_player_car.c" + ], + "functions": [ + { + "name": "UpdatePlayerCar", + "path": "src/main/PAL/main/car/update_player_car.c", + "lines": [ + 24, + 527 + ] + } + ], + "dependsOn": [ + "drivetrain", + "track-contact", + "body-motion" + ] + }, + { + "id": "drivetrain", + "name": "Engine and gearbox model", + "purpose": "Integer engine-speed simulation with banded torque curves, a rev limiter, clutch-blended gear changes, slope load, aerodynamic drag and the per-update speed retention that turns engine output into road speed", + "paths": [ + "src/main/PAL/main/car/update_car_drivetrain.c" + ], + "functions": [ + { + "name": "UpdateCarDrivetrain", + "path": "src/main/PAL/main/car/update_car_drivetrain.c", + "lines": [ + 17, + 841 + ] + } + ], + "dependsOn": [ + "motion-states" + ] + }, + { + "id": "motion-states", + "name": "Driving, slide and start handlers", + "purpose": "The four handlers the drivetrain dispatches to each update: normal grip driving, the slide entry pass, the slide recovery pass and the standing-start wheelspin, plus the shared position and heading integrator", + "paths": [ + "src/main/PAL/main/car/update_car_driving.c", + "src/main/PAL/main/car/steer_car_to_track_line.c", + "src/main/PAL/main/car/advance_car_position.c" + ], + "functions": [ + { + "name": "UpdateCarDriving", + "path": "src/main/PAL/main/car/update_car_driving.c", + "lines": [ + 15, + 108 + ] + }, + { + "name": "UpdateCarLaunch", + "path": "src/main/PAL/main/car/steer_car_to_track_line.c", + "lines": [ + 96, + 313 + ] + }, + { + "name": "UpdateCarAirborne", + "path": "src/main/PAL/main/car/steer_car_to_track_line.c", + "lines": [ + 320, + 386 + ] + }, + { + "name": "UpdateCarStandingStart", + "path": "src/main/PAL/main/car/steer_car_to_track_line.c", + "lines": [ + 388, + 445 + ] + }, + { + "name": "AdvanceCarPosition", + "path": "src/main/PAL/main/car/advance_car_position.c", + "lines": [ + 13, + 48 + ] + }, + { + "name": "SteerCarToTrackLine", + "path": "src/main/PAL/main/car/steer_car_to_track_line.c", + "lines": [ + 22, + 86 + ] + } + ], + "dependsOn": [ + "track-contact" + ] + }, + { + "id": "track-contact", + "name": "Track-relative placement", + "purpose": "Projects each car onto the spline-and-arc track description to get distance along the segment, lateral offset, surface height, pitch, roll and lap progress, and pushes the car back inside the road edges", + "paths": [ + "src/main/PAL/main/car/car_track_state.c", + "src/main/PAL/main/car/clamp_car_lateral_offset.c", + "src/main/PAL/main/car/steer_car_to_track_line.c" + ], + "functions": [ + { + "name": "UpdateCarTrackState", + "path": "src/main/PAL/main/car/car_track_state.c", + "lines": [ + 28, + 396 + ] + }, + { + "name": "SampleTrackSurfaceHeight", + "path": "src/main/PAL/main/car/car_track_state.c", + "lines": [ + 407, + 449 + ] + }, + { + "name": "FindTrackSegment", + "path": "src/main/PAL/main/car/steer_car_to_track_line.c", + "lines": [ + 456, + 541 + ] + }, + { + "name": "ClampCarLateralOffset", + "path": "src/main/PAL/main/car/clamp_car_lateral_offset.c", + "lines": [ + 15, + 70 + ] + } + ], + "dependsOn": [] + }, + { + "id": "body-motion", + "name": "Impacts, crests and body effects", + "purpose": "Wall knockback vectors, crest hop arcs triggered by track markers, impact body kicks and the throttle and brake squat that tilts the body", + "paths": [ + "src/main/PAL/main/car/car_draw_and_knockback.c", + "src/main/PAL/main/car/car_ai.c" + ], + "functions": [ + { + "name": "SetCarKnockback", + "path": "src/main/PAL/main/car/car_draw_and_knockback.c", + "lines": [ + 226, + 353 + ] + }, + { + "name": "ApplyCarKnockback", + "path": "src/main/PAL/main/car/car_draw_and_knockback.c", + "lines": [ + 199, + 215 + ] + }, + { + "name": "StartCarBodyKick", + "path": "src/main/PAL/main/car/car_draw_and_knockback.c", + "lines": [ + 355, + 414 + ] + }, + { + "name": "UpdateCarTiltCounter", + "path": "src/main/PAL/main/car/car_draw_and_knockback.c", + "lines": [ + 143, + 197 + ] + }, + { + "name": "UpdateCarBodyKick", + "path": "src/main/PAL/main/car/car_ai.c", + "lines": [ + 15, + 73 + ] + }, + { + "name": "GetCarCrestTrigger", + "path": "src/main/PAL/main/car/car_ai.c", + "lines": [ + 75, + 155 + ] + }, + { + "name": "UpdateCarCrestHop", + "path": "src/main/PAL/main/car/car_ai.c", + "lines": [ + 167, + 239 + ] + } + ], + "dependsOn": [ + "track-contact" + ] + }, + { + "id": "camera", + "name": "Race cameras", + "purpose": "Bumper view, lagging chase view with three height presets, and the track-side and scripted path cameras chosen by the nearest camera node", + "paths": [ + "src/main/PAL/main/track/update_camera.c", + "src/main/PAL/main/track/find_nearest_track_camera.c" + ], + "functions": [ + { + "name": "UpdateCamera", + "path": "src/main/PAL/main/track/update_camera.c", + "lines": [ + 37, + 635 + ] + }, + { + "name": "FindNearestTrackCamera", + "path": "src/main/PAL/main/track/find_nearest_track_camera.c", + "lines": [ + 6, + 66 + ] + } + ], + "dependsOn": [ + "track-contact" + ] + }, + { + "id": "rival-ai", + "name": "Rival driver logic", + "purpose": "Per-rival speed schedule read from track keys, lateral racing-line hints, traffic avoidance between cars, AI slide flicks and the rubber band that holds the leading contenders near the player", + "paths": [ + "src/main/PAL/main/car/race_car_update.c", + "src/main/PAL/main/car/rival_ai.c", + "src/main/PAL/main/car/car_ai.c", + "src/main/PAL/main/car/clamp_car_lateral_offset.c" + ], + "functions": [ + { + "name": "UpdateRaceCars", + "path": "src/main/PAL/main/car/race_car_update.c", + "lines": [ + 18, + 289 + ] + }, + { + "name": "UpdateCarAiTargetSpeed", + "path": "src/main/PAL/main/car/car_ai.c", + "lines": [ + 415, + 515 + ] + }, + { + "name": "UpdateCarSlideAngle", + "path": "src/main/PAL/main/car/car_ai.c", + "lines": [ + 241, + 316 + ] + }, + { + "name": "ApplyCarRacingLineHint", + "path": "src/main/PAL/main/car/car_ai.c", + "lines": [ + 318, + 384 + ] + }, + { + "name": "UpdateCarTrafficAvoidance", + "path": "src/main/PAL/main/car/rival_ai.c", + "lines": [ + 30, + 190 + ] + }, + { + "name": "SlowRivalAhead", + "path": "src/main/PAL/main/car/rival_ai.c", + "lines": [ + 192, + 215 + ] + }, + { + "name": "RankContenders", + "path": "src/main/PAL/main/car/rival_ai.c", + "lines": [ + 223, + 279 + ] + }, + { + "name": "UpdateRivalRubberBand", + "path": "src/main/PAL/main/car/rival_ai.c", + "lines": [ + 282, + 407 + ] + } + ], + "dependsOn": [ + "track-contact", + "motion-states" + ] + } + ], + "cards": [ + { + "id": "engine-torque-bands", + "node": "drivetrain", + "name": "Banded engine torque and rev limiter", + "summary": "Engine speed picks a thousand-rpm band whose torque is linearly blended per gear, and a soft limiter pulls revs back past the cutoff", + "behavior": "Engine speed is divided into thousand-rpm bands. For the current band the model looks up a pair of rpm breakpoints and two per-gear torque values, then blends linearly between them, dividing by ten times the band width. A second table blended the same way yields a percentage loss that later reduces the drag term, and first gear doubles that percentage below the redline. Once engine speed reaches the rev limit the torque is replaced by four fifths of the overshoot, which is negative, so the engine falls back under the cutoff. Throttle input scales the final torque in 1/256 steps and the clutch flag gates it to zero while a shift is engaging. Engine speed is clamped to the range from zero to 15000 after every update.", + "constants": [ + { + "name": "rpmBandWidth", + "value": 1000, + "unit": "rpm per band, per update", + "source": "src/main/PAL/main/car/update_car_drivetrain.c:304" + }, + { + "name": "torqueBlendScale", + "value": "0xA", + "unit": "multiplier on band width, per update", + "source": "src/main/PAL/main/car/update_car_drivetrain.c:341" + }, + { + "name": "limiterPullback", + "value": 4, + "unit": "fifths of the overshoot removed, per update, rate unverified", + "source": "src/main/PAL/main/car/update_car_drivetrain.c:300" + }, + { + "name": "engineRpmCeiling", + "value": "0x3A98", + "unit": "rpm, clamped per update, rate unverified", + "source": "src/main/PAL/main/car/update_car_drivetrain.c:709" + } + ], + "sources": [ + { + "path": "src/main/PAL/main/car/update_car_drivetrain.c", + "line": 296 + }, + { + "path": "src/main/PAL/main/car/update_car_drivetrain.c", + "line": 304 + }, + { + "path": "src/main/PAL/main/car/update_car_drivetrain.c", + "line": 341 + }, + { + "path": "src/main/PAL/main/car/update_car_drivetrain.c", + "line": 565 + }, + { + "path": "src/main/PAL/main/car/update_car_drivetrain.c", + "line": 707 + } + ], + "capabilities": [ + "torque curve", + "rev limiter", + "throttle response" + ], + "tags": [ + "engine", + "acceleration", + "physics", + "tuning" + ] + }, + { + "id": "gear-shift-blend", + "node": "drivetrain", + "name": "Clutch-blended gear change", + "summary": "A gear change computes the target revs for the new ratio and eases the tachometer there over ten updates", + "behavior": "When the selected gear differs from the engaged gear the model converts road speed into the engine speed the new ratio demands. A downshift adds 500 rpm to that target as a blip. The clutch counter is set to ten and drive is uncoupled, so no throttle torque reaches the wheels. Each update the counter falls and engine speed is placed between the old and new values: the manual gearbox spreads the move over fifteen steps of the remaining gap, the automatic over ten. When the counter reaches zero drive couples again. With the manual gearbox, an upshift on a climb, the grade sign that loads the engine, reduces the shift target and the carried engine load by a grade fraction that grows from fourth to sixth gear, so the engine lands lower in the band on a hill.", + "constants": [ + { + "name": "clutchUpdates", + "value": "0xA", + "unit": "updates, per update, rate unverified", + "source": "src/main/PAL/main/car/update_car_drivetrain.c:531" + }, + { + "name": "downshiftBlip", + "value": "0x1F4", + "unit": "rpm added to the target", + "source": "src/main/PAL/main/car/update_car_drivetrain.c:525" + }, + { + "name": "manualBlendSteps", + "value": 15, + "unit": "steps over the gap, per update, rate unverified", + "source": "src/main/PAL/main/car/update_car_drivetrain.c:549" + }, + { + "name": "automaticBlendSteps", + "value": 10, + "unit": "steps over the gap, per update, rate unverified", + "source": "src/main/PAL/main/car/update_car_drivetrain.c:554" + }, + { + "name": "fourthGearGradeDivisor", + "value": 120, + "unit": "grade units per percent of shift target", + "source": "src/main/PAL/main/car/update_car_drivetrain.c:492" + }, + { + "name": "fifthGearGradeDivisor", + "value": 48, + "unit": "grade units per percent of shift target", + "source": "src/main/PAL/main/car/update_car_drivetrain.c:499" + } + ], + "sources": [ + { + "path": "src/main/PAL/main/car/update_car_drivetrain.c", + "line": 470 + }, + { + "path": "src/main/PAL/main/car/update_car_drivetrain.c", + "line": 474 + }, + { + "path": "src/main/PAL/main/car/update_car_drivetrain.c", + "line": 525 + }, + { + "path": "src/main/PAL/main/car/update_car_drivetrain.c", + "line": 531 + }, + { + "path": "src/main/PAL/main/car/update_car_drivetrain.c", + "line": 549 + } + ], + "capabilities": [ + "gear shift", + "automatic gearbox", + "manual gearbox", + "rev matching" + ], + "tags": [ + "engine", + "transmission", + "feel" + ] + }, + { + "id": "speed-retention-drag", + "node": "drivetrain", + "name": "Road speed retention and drag", + "summary": "Road speed keeps 94 percent of itself each update while drivetrain torque supplies the acceleration term, and a speed-squared drag loads the engine", + "behavior": "In grip driving the car keeps 94 hundredths of its speed every update and the shared integrator adds back the acceleration derived from drivetrain torque, so top speed emerges where the two balance instead of from a hard cap. The automatic gearbox scales that acceleration by a per-car factor in thousandths, and a grip-loss timer halves it while it runs. Engine load grows with the square of the displayed road speed divided by a per-car drag divisor times a thousand, and brake input adds load in proportion to engine speed. Road speed for the display and the drag uses a 160 over 1168 conversion from internal speed units. Below eight speed units the travel heading snaps to the body yaw so a stopped car does not drift sideways.", + "constants": [ + { + "name": "gripSpeedRetention", + "value": "0x5E", + "unit": "hundredths of speed kept, per update, rate unverified", + "source": "src/main/PAL/main/car/update_car_drivetrain.c:801" + }, + { + "name": "roadSpeedDivisor", + "value": 1168, + "unit": "internal speed units per display conversion", + "source": "src/main/PAL/main/car/update_car_drivetrain.c:679" + }, + { + "name": "dragDivisorScale", + "value": "0x3E8", + "unit": "multiplier on the per-car drag divisor", + "source": "src/main/PAL/main/car/update_car_drivetrain.c:680" + }, + { + "name": "headingSnapSpeed", + "value": 8, + "unit": "internal speed units", + "source": "src/main/PAL/main/car/update_car_drivetrain.c:805" + } + ], + "sources": [ + { + "path": "src/main/PAL/main/car/update_car_drivetrain.c", + "line": 679 + }, + { + "path": "src/main/PAL/main/car/update_car_drivetrain.c", + "line": 686 + }, + { + "path": "src/main/PAL/main/car/update_car_drivetrain.c", + "line": 798 + }, + { + "path": "src/main/PAL/main/car/update_car_drivetrain.c", + "line": 801 + }, + { + "path": "src/main/PAL/main/car/update_car_drivetrain.c", + "line": 805 + } + ], + "capabilities": [ + "speed retention", + "aerodynamic drag", + "emergent top speed" + ], + "tags": [ + "speed", + "physics", + "drag", + "top speed" + ] + }, + { + "id": "slope-engine-load", + "node": "drivetrain", + "name": "Slope load on the engine", + "summary": "The blended track pitch under the car, turned to the car heading, loads the engine fully uphill and only a tenth as much downhill", + "behavior": "The pitch values of the current and next track points are blended by how far the car is along the segment, then multiplied by the cosine of the angle between the car heading and the track direction, so driving across a slope feels flatter than driving straight up it. The result is clamped to plus or minus 238 angle units and stored as the road grade the gearbox reads. The sine of that grade times 1800, divided by 40960, becomes extra engine load. On a climb the full term adds load; on a descent a tenth of it is removed, a mild assist. A separate countdown adds 200 load plus twenty per remaining update while it runs. The same pass adds load from the angle between body yaw and travel heading, so a sideways car bogs down.", + "constants": [ + { + "name": "gradeClamp", + "value": "0xEE", + "unit": "angle units, 4096 per turn", + "source": "src/main/PAL/main/car/update_car_drivetrain.c:648" + }, + { + "name": "gradeLoadScale", + "value": "0x708", + "unit": "load units per unit of sine", + "source": "src/main/PAL/main/car/update_car_drivetrain.c:650" + }, + { + "name": "gradeLoadDivisor", + "value": "0xA000", + "unit": "divisor on the scaled sine", + "source": "src/main/PAL/main/car/update_car_drivetrain.c:652" + }, + { + "name": "descentShare", + "value": 10, + "unit": "divisor applied on a descent", + "source": "src/main/PAL/main/car/update_car_drivetrain.c:660" + } + ], + "sources": [ + { + "path": "src/main/PAL/main/car/update_car_drivetrain.c", + "line": 620 + }, + { + "path": "src/main/PAL/main/car/update_car_drivetrain.c", + "line": 625 + }, + { + "path": "src/main/PAL/main/car/update_car_drivetrain.c", + "line": 650 + }, + { + "path": "src/main/PAL/main/car/update_car_drivetrain.c", + "line": 660 + }, + { + "path": "src/main/PAL/main/car/update_car_drivetrain.c", + "line": 601 + } + ], + "capabilities": [ + "slope resistance", + "grade-aware load", + "sideways bog" + ], + "tags": [ + "physics", + "terrain", + "engine" + ] + }, + { + "id": "slide-grip-budget", + "node": "drivetrain", + "name": "Pedal-driven steering grip", + "summary": "A steering grip budget falls with throttle and rises with brake, and banked corners add or remove grip by direction", + "behavior": "Every update the model builds a grip budget starting at 380, subtracts throttle input scaled to at most 100, and adds brake input scaled the same way, so lifting or braking tightens the line and flooring it loosens it. While the car is outside the slide entry state and on a curve whose direction differs from the remembered one, the cross slope of the road, clamped to plus or minus 50, adds grip: three times the slope on one hand of curve and minus three times the slope on the other, so the bank changes grip by curve direction. The stored grip moves halfway toward the new budget each update, weighted by a per-car response value in thousandths. In the slide entry state a curve bias that builds up while the curve direction stays the same, clamped to plus or minus 30, takes ten grip units per step.", + "constants": [ + { + "name": "gripBase", + "value": "0x17C", + "unit": "grip units", + "source": "src/main/PAL/main/car/update_car_drivetrain.c:183" + }, + { + "name": "camberClamp", + "value": "0x32", + "unit": "cross-slope units", + "source": "src/main/PAL/main/car/update_car_drivetrain.c:234" + }, + { + "name": "camberGain", + "value": 3, + "unit": "grip units per cross-slope unit", + "source": "src/main/PAL/main/car/update_car_drivetrain.c:247" + }, + { + "name": "curveBiasClamp", + "value": "0x1E", + "unit": "bias steps", + "source": "src/main/PAL/main/car/update_car_drivetrain.c:216" + }, + { + "name": "gripResponseScale", + "value": 1000, + "unit": "thousandths per update", + "source": "src/main/PAL/main/car/update_car_drivetrain.c:251" + } + ], + "sources": [ + { + "path": "src/main/PAL/main/car/update_car_drivetrain.c", + "line": 177 + }, + { + "path": "src/main/PAL/main/car/update_car_drivetrain.c", + "line": 183 + }, + { + "path": "src/main/PAL/main/car/update_car_drivetrain.c", + "line": 222 + }, + { + "path": "src/main/PAL/main/car/update_car_drivetrain.c", + "line": 243 + }, + { + "path": "src/main/PAL/main/car/update_car_drivetrain.c", + "line": 251 + } + ], + "capabilities": [ + "steering grip", + "banked corner grip", + "pedal balance" + ], + "tags": [ + "handling", + "steering", + "grip", + "feel" + ] + }, + { + "id": "slide-entry", + "node": "motion-states", + "name": "Throttle-stab and brake-tap slide entry", + "summary": "Lifting then stabbing the throttle, or tapping the brake while turning, commits the car to a slide when it carries enough energy", + "behavior": "Each pedal has a three-step latch: it arms above 133 of 255, reports a fresh press for exactly one update, then holds until the pedal drops below 124. While the throttle is under half and the brake is not freshly pressed, a counter of coasting updates grows. A fresh throttle press multiplies speed by that coasting count to form slide energy; if speed clears the course threshold and energy clears the car threshold, the car enters the slide state with a spin rate set from twice its lateral velocity, sharpened when the car steering response is below nominal. A fresh brake press instead uses the absolute lateral velocity times speed over 64, against a second speed threshold, and seeds spin from the lateral velocity alone. Outside those paths the coasting count and energy reset.", + "constants": [ + { + "name": "pedalArmLevel", + "value": "0x85", + "unit": "pedal units out of 255", + "source": "src/main/PAL/main/car/update_car_drivetrain.c:144" + }, + { + "name": "pedalReleaseLevel", + "value": "0x7C", + "unit": "pedal units out of 255", + "source": "src/main/PAL/main/car/update_car_drivetrain.c:155" + }, + { + "name": "coastThrottleLevel", + "value": 128, + "unit": "pedal units out of 255", + "source": "src/main/PAL/main/car/update_car_driving.c:85" + }, + { + "name": "brakeTapEnergyDivisor", + "value": 64, + "unit": "speed times lateral velocity per energy unit", + "source": "src/main/PAL/main/car/update_car_driving.c:89" + }, + { + "name": "throttleStabSpinGain", + "value": 2, + "unit": "multiplier on lateral velocity", + "source": "src/main/PAL/main/car/update_car_driving.c:81" + } + ], + "sources": [ + { + "path": "src/main/PAL/main/car/update_car_driving.c", + "line": 69 + }, + { + "path": "src/main/PAL/main/car/update_car_driving.c", + "line": 70 + }, + { + "path": "src/main/PAL/main/car/update_car_driving.c", + "line": 87 + }, + { + "path": "src/main/PAL/main/car/update_car_drivetrain.c", + "line": 141 + }, + { + "path": "src/main/PAL/main/car/update_car_drivetrain.c", + "line": 159 + } + ], + "capabilities": [ + "drift initiation", + "pedal latch", + "technique input" + ], + "tags": [ + "drift", + "technique", + "handling", + "input" + ] + }, + { + "id": "slide-rotation", + "node": "motion-states", + "name": "Slide rotation and energy drain", + "summary": "During a slide the body yaw spins away from the travel heading under a capped spin rate while an energy budget drains each update", + "behavior": "The source labels this state a takeoff; in play it is the committed slide. While slide energy remains, body yaw advances by the spin rate over 256 and the spin rate is pulled toward the steering target, scaled by the steering load, with a cap of plus or minus 13824. With the steering short of full lock and the body almost aligned, spin decays by one thirty-second and is drawn back toward the heading. Energy drains a fixed 64 per update, plus the square of the yaw gap over 65536, plus terms for low spin, low speed, brake and lifted throttle; brake and lift also scrub speed. A body more than 1536 angle units off heading loses one percent of speed per update. A body lift offset climbs by ten to a cap of 100. When energy runs out the car hands to recovery with a twenty-update timer, keeps part of its speed as sideways momentum based on the yaw gap, and starts a rev blend for the current gear.", + "constants": [ + { + "name": "spinRateCap", + "value": "0x3600", + "unit": "spin units, 256 per angle unit per update", + "source": "src/main/PAL/main/car/steer_car_to_track_line.c:179" + }, + { + "name": "energyDrainPerUpdate", + "value": 64, + "unit": "energy units, per update, rate unverified", + "source": "src/main/PAL/main/car/steer_car_to_track_line.c:187" + }, + { + "name": "overRotationRetention", + "value": 990, + "unit": "thousandths of speed kept, per update, rate unverified", + "source": "src/main/PAL/main/car/steer_car_to_track_line.c:121" + }, + { + "name": "overRotationThreshold", + "value": "0x600", + "unit": "angle units, 4096 per turn", + "source": "src/main/PAL/main/car/steer_car_to_track_line.c:120" + }, + { + "name": "bodyLiftStep", + "value": 10, + "unit": "lift units, per update, rate unverified", + "source": "src/main/PAL/main/car/steer_car_to_track_line.c:158" + }, + { + "name": "recoveryUpdates", + "value": "0x14", + "unit": "updates, per update, rate unverified", + "source": "src/main/PAL/main/car/steer_car_to_track_line.c:257" + } + ], + "sources": [ + { + "path": "src/main/PAL/main/car/steer_car_to_track_line.c", + "line": 96 + }, + { + "path": "src/main/PAL/main/car/steer_car_to_track_line.c", + "line": 163 + }, + { + "path": "src/main/PAL/main/car/steer_car_to_track_line.c", + "line": 186 + }, + { + "path": "src/main/PAL/main/car/steer_car_to_track_line.c", + "line": 190 + }, + { + "path": "src/main/PAL/main/car/steer_car_to_track_line.c", + "line": 236 + } + ], + "capabilities": [ + "drift", + "yaw spin", + "energy budget", + "slide exit" + ], + "tags": [ + "drift", + "handling", + "physics", + "feel" + ] + }, + { + "id": "slide-recovery", + "node": "motion-states", + "name": "Slide recovery glide", + "summary": "After a slide the car glides for twenty updates while leftover sideways momentum, spin and yaw offset decay by a thirty-second each update", + "behavior": "The source labels this state airborne; it runs after the slide energy is spent. Body yaw keeps chasing the steering target by a fifth of the gap. Travel velocity is rebuilt from the heading plus the stored yaw offset, and the momentum captured at slide exit is added along the exit heading. Spin, captured momentum and yaw offset each keep 31/32 per update and the body lift keeps two thirds. If the yaw offset is still more than 1536 angle units, speed drops to four fifths each update, which punishes a slide held too far. When the twenty-update timer expires, the remaining spin is removed from body yaw and the car returns to grip driving. A coasting counter keeps running here, so a driver can chain the next slide straight out of recovery.", + "constants": [ + { + "name": "recoveryDecay", + "value": 31, + "unit": "thirty-seconds kept, per update, rate unverified", + "source": "src/main/PAL/main/car/steer_car_to_track_line.c:367" + }, + { + "name": "liftDecayDenominator", + "value": 3, + "unit": "thirds, two of which are kept, per update, rate unverified", + "source": "src/main/PAL/main/car/steer_car_to_track_line.c:371" + }, + { + "name": "overRotationLimit", + "value": 1537, + "unit": "angle units, 4096 per turn", + "source": "src/main/PAL/main/car/steer_car_to_track_line.c:372" + }, + { + "name": "overRotationSpeedKeep", + "value": 4, + "unit": "fifths of speed kept, per update, rate unverified", + "source": "src/main/PAL/main/car/steer_car_to_track_line.c:373" + } + ], + "sources": [ + { + "path": "src/main/PAL/main/car/steer_car_to_track_line.c", + "line": 320 + }, + { + "path": "src/main/PAL/main/car/steer_car_to_track_line.c", + "line": 350 + }, + { + "path": "src/main/PAL/main/car/steer_car_to_track_line.c", + "line": 367 + }, + { + "path": "src/main/PAL/main/car/steer_car_to_track_line.c", + "line": 376 + }, + { + "path": "src/main/PAL/main/car/steer_car_to_track_line.c", + "line": 383 + } + ], + "capabilities": [ + "drift recovery", + "momentum carry", + "slide chaining" + ], + "tags": [ + "drift", + "recovery", + "physics" + ] + }, + { + "id": "heading-integration", + "node": "motion-states", + "name": "Split heading and body yaw", + "summary": "The car keeps a travel heading separate from its body yaw, and the body yaw chases the steering target by one fifth of the gap each update", + "behavior": "Each motion handler first moves body yaw a fifth of the way toward the steering target heading. The integrator then builds a displacement from a quarter of speed along the travel heading plus the acceleration term along the body yaw, divided by 100, and recomputes the travel heading from that displacement. Speed itself grows by the acceleration term times the cosine of the gap between heading and body yaw, so thrust only builds speed in the direction the car points. In grip driving a velocity is then rebuilt by projecting the travel vector onto the body axis, which removes lateral slip; the standing-start handler projects with a four times larger divisor. Angles are 12-bit, 4096 to a turn, with sine and cosine in 4096 fixed point.", + "constants": [ + { + "name": "yawChaseDivisor", + "value": 5, + "unit": "fraction of the yaw gap per update", + "source": "src/main/PAL/main/car/update_car_driving.c:29" + }, + { + "name": "speedShareDivisor", + "value": 4, + "unit": "divisor on speed in the displacement", + "source": "src/main/PAL/main/car/advance_car_position.c:22" + }, + { + "name": "displacementDivisor", + "value": 100, + "unit": "divisor per update", + "source": "src/main/PAL/main/car/advance_car_position.c:22" + }, + { + "name": "fixedPointOne", + "value": 4096, + "unit": "fixed-point unit for sine and cosine", + "source": "src/main/PAL/main/car/advance_car_position.c:45" + } + ], + "sources": [ + { + "path": "src/main/PAL/main/car/update_car_driving.c", + "line": 27 + }, + { + "path": "src/main/PAL/main/car/advance_car_position.c", + "line": 13 + }, + { + "path": "src/main/PAL/main/car/advance_car_position.c", + "line": 22 + }, + { + "path": "src/main/PAL/main/car/advance_car_position.c", + "line": 45 + }, + { + "path": "src/main/PAL/main/car/advance_car_position.c", + "line": 47 + } + ], + "capabilities": [ + "heading integration", + "yaw smoothing", + "slip removal" + ], + "tags": [ + "movement", + "steering", + "physics" + ] + }, + { + "id": "standing-start", + "node": "motion-states", + "name": "Standing-start wheelspin", + "summary": "At the start a spin counter burns down with throttle and brake while speed is cut to a tenth and the body jitters", + "behavior": "If the race starts with the car in the standing-start state, every update divides speed by ten and burns a spin counter down by the throttle input plus 32, plus 1000 more while engine speed is below 2000, plus 127 more when the throttle is under half at higher revs. Brake input burns twice its value as well. The body jitters vertically and sideways by small random amounts scaled to the burn. The drivetrain adds a fraction of the remaining spin to engine load so revs flare. When the counter is used up, or starts at ten or less, the car changes to grip driving. The tyre squeal volume falls as the counter drains.", + "constants": [ + { + "name": "startSpeedDivisor", + "value": 10, + "unit": "divisor on speed, per update, rate unverified", + "source": "src/main/PAL/main/car/steer_car_to_track_line.c:416" + }, + { + "name": "lowRevThreshold", + "value": 2000, + "unit": "rpm", + "source": "src/main/PAL/main/car/steer_car_to_track_line.c:424" + }, + { + "name": "lowRevExtraBurn", + "value": 1032, + "unit": "spin units, throttle added, per update, rate unverified", + "source": "src/main/PAL/main/car/steer_car_to_track_line.c:425" + }, + { + "name": "minimumSpinToStart", + "value": 11, + "unit": "spin units", + "source": "src/main/PAL/main/car/steer_car_to_track_line.c:418" + } + ], + "sources": [ + { + "path": "src/main/PAL/main/car/steer_car_to_track_line.c", + "line": 388 + }, + { + "path": "src/main/PAL/main/car/steer_car_to_track_line.c", + "line": 416 + }, + { + "path": "src/main/PAL/main/car/steer_car_to_track_line.c", + "line": 430 + }, + { + "path": "src/main/PAL/main/car/steer_car_to_track_line.c", + "line": 433 + }, + { + "path": "src/main/PAL/main/car/update_car_drivetrain.c", + "line": 664 + } + ], + "capabilities": [ + "standing start", + "wheelspin", + "launch control" + ], + "tags": [ + "start", + "timing", + "technique" + ] + }, + { + "id": "track-surface-placement", + "node": "track-contact", + "name": "Track-frame height, pitch and roll", + "summary": "The car is placed by distance along the current track segment and lateral offset, and its height, pitch and roll come from interpolated track data", + "behavior": "Each track point stores position, heading, left and right half widths, height, cross slope and pitch. Straight segments project the car into the segment frame; curved segments measure the car radius about an arc centre and blend the heading by swept angle. Along-segment distance is clamped to the segment and stored as a fraction of 1024. Half widths, height, cross slope and pitch are blended between the two points by that distance. Height is the blended height plus the cross slope times the lateral offset over 128. Body pitch and roll are the track pitch and the bank angle, turned into the car frame with the sine and cosine of the car yaw relative to the track. A separate sampler finds the containing segment quad by spiralling outward from the last index.", + "constants": [ + { + "name": "crossSlopeShift", + "value": 7, + "unit": "bits, so 128 lateral units per slope unit", + "source": "src/main/PAL/main/car/car_track_state.c:319" + }, + { + "name": "segmentFractionShift", + "value": "0xA", + "unit": "bits, 1024 per segment", + "source": "src/main/PAL/main/car/car_track_state.c:286" + }, + { + "name": "localOffsetScale", + "value": 4, + "unit": "multiplier before rotation into the segment frame", + "source": "src/main/PAL/main/car/car_track_state.c:205" + } + ], + "sources": [ + { + "path": "src/main/PAL/main/car/car_track_state.c", + "line": 28 + }, + { + "path": "src/main/PAL/main/car/car_track_state.c", + "line": 112 + }, + { + "path": "src/main/PAL/main/car/car_track_state.c", + "line": 286 + }, + { + "path": "src/main/PAL/main/car/car_track_state.c", + "line": 319 + }, + { + "path": "src/main/PAL/main/car/car_track_state.c", + "line": 358 + }, + { + "path": "src/main/PAL/main/car/car_track_state.c", + "line": 373 + } + ], + "capabilities": [ + "spline track", + "arc segments", + "banked road", + "surface height" + ], + "tags": [ + "physics", + "terrain", + "track", + "spatial test" + ] + }, + { + "id": "wall-knockback", + "node": "body-motion", + "name": "Wall push and knockback", + "summary": "The outermost body corner past a road edge pushes the car back inside and, for the player, starts a decaying knockback whose strength depends on speed and which corner hit", + "behavior": "The player car rotates its four corner offsets by the yaw relative to the track and keeps the farthest left and right corners as the edge insets, noting which corner each was. If the lateral offset passes an edge plus its inset, the car moves back onto the edge and, when it is the player, a knockback starts. A hit on the first corner pushes away from the wall at ten units plus a speed term: above 800 speed units the term is speed, capped at 1800, times the sine of the yaw against the wall over 65536, otherwise a small fixed share of that sine. That push lasts thirty updates. Hits on the second or third corner push twenty units for fifteen updates, and a fourth-corner hit halves the impact vector for fifteen updates. The knockback displaces the car each update and keeps seven eighths of itself. Rivals are clamped to five eighths or four sevenths of the half width.", + "constants": [ + { + "name": "speedPushThreshold", + "value": "0x321", + "unit": "speed units", + "source": "src/main/PAL/main/car/car_draw_and_knockback.c:265" + }, + { + "name": "speedPushCap", + "value": "0x708", + "unit": "speed units", + "source": "src/main/PAL/main/car/car_draw_and_knockback.c:267" + }, + { + "name": "pushBase", + "value": 10, + "unit": "world units per update", + "source": "src/main/PAL/main/car/car_draw_and_knockback.c:293" + }, + { + "name": "firstCornerHitUpdates", + "value": "0x1E", + "unit": "updates, per update, rate unverified", + "source": "src/main/PAL/main/car/car_draw_and_knockback.c:310" + }, + { + "name": "sideHitPush", + "value": 20, + "unit": "world units per update", + "source": "src/main/PAL/main/car/car_draw_and_knockback.c:326" + }, + { + "name": "knockbackRetention", + "value": 7, + "unit": "eighths kept, per update, rate unverified", + "source": "src/main/PAL/main/car/car_draw_and_knockback.c:212" + } + ], + "sources": [ + { + "path": "src/main/PAL/main/car/update_player_car.c", + "line": 274 + }, + { + "path": "src/main/PAL/main/car/car_track_state.c", + "line": 238 + }, + { + "path": "src/main/PAL/main/car/car_track_state.c", + "line": 249 + }, + { + "path": "src/main/PAL/main/car/car_draw_and_knockback.c", + "line": 226 + }, + { + "path": "src/main/PAL/main/car/car_draw_and_knockback.c", + "line": 210 + }, + { + "path": "src/main/PAL/main/car/clamp_car_lateral_offset.c", + "line": 55 + } + ], + "capabilities": [ + "wall collision", + "knockback", + "edge clamp" + ], + "tags": [ + "physics", + "collision", + "hit reaction" + ] + }, + { + "id": "crest-hop", + "node": "body-motion", + "name": "Marker-triggered crest hop", + "summary": "Crossing a track crest marker above 800 speed units starts a vertical hop whose rate scales with speed, and the arc grows with the square of time", + "behavior": "Each track carries up to eight crest markers per direction, each holding a progress value and a signed strength. Above 800 speed units, the hop logic compares the progress values of this update and the previous one with every marker. A jump of 4096 or more between the two is treated as a lap wrap and ignored. A positive strength starts a hop whose vertical rate is strength times speed over minus 4800, so a faster car leaves the crest harder. A negative strength starts a second mode that uses the magnitude of the strength as the rate, without the speed term. While a hop runs, the pitch accumulator gains the square of the hop timer over six, divided by 256 once the accumulator reaches 300, and the roll accumulator gains a quarter of that. The car pitch and roll are then overridden by the accumulators.", + "constants": [ + { + "name": "crestSpeedThreshold", + "value": "0x320", + "unit": "speed units", + "source": "src/main/PAL/main/car/car_ai.c:93" + }, + { + "name": "lapWrapGuard", + "value": "0x1000", + "unit": "progress units", + "source": "src/main/PAL/main/car/car_ai.c:122" + }, + { + "name": "hopRateDivisor", + "value": -4800, + "unit": "strength times speed per rate unit", + "source": "src/main/PAL/main/car/car_ai.c:222" + }, + { + "name": "arcDivisor", + "value": 6, + "unit": "divisor on timer squared, per update, rate unverified", + "source": "src/main/PAL/main/car/car_ai.c:185" + }, + { + "name": "crestMarkersPerDirection", + "value": 8, + "unit": "markers", + "source": "src/main/PAL/main/car/car_ai.c:147" + } + ], + "sources": [ + { + "path": "src/main/PAL/main/car/car_ai.c", + "line": 75 + }, + { + "path": "src/main/PAL/main/car/car_ai.c", + "line": 93 + }, + { + "path": "src/main/PAL/main/car/car_ai.c", + "line": 167 + }, + { + "path": "src/main/PAL/main/car/car_ai.c", + "line": 213 + }, + { + "path": "src/main/PAL/main/car/car_ai.c", + "line": 222 + } + ], + "capabilities": [ + "jump", + "crest", + "vertical motion" + ], + "tags": [ + "jump", + "physics", + "terrain", + "feel" + ] + }, + { + "id": "body-kick-and-squat", + "node": "body-motion", + "name": "Impact kick and pedal squat", + "summary": "Impacts start a thirty-update decaying sine on pitch or roll, and heavy throttle or braking tilts the body", + "behavior": "A wall or car contact starts a body kick. The angled kick measures the yaw against the track, and above 320 speed units its amplitude is the excess speed times that angle, given a random sign. The kick runs thirty updates. Each update the amplitude is scaled by the remaining time over 128 and a sine of three cycles over the thirty updates rides on top, then goes to pitch, roll or both by kick mode. Separately, a tilt counter moves one way by four per update while the throttle is past half at or above the redline, down to a floor of five times nine minus the gear, so lower gears squat harder. Above 80 speed units, braking or sliding moves it the other way by two to a cap of eight. Otherwise it relaxes to three quarters each update. The bumper camera adds this counter to its pitch.", + "constants": [ + { + "name": "kickUpdates", + "value": "0x1E", + "unit": "updates, per update, rate unverified", + "source": "src/main/PAL/main/car/car_draw_and_knockback.c:376" + }, + { + "name": "kickSpeedThreshold", + "value": "0x140", + "unit": "speed units", + "source": "src/main/PAL/main/car/car_draw_and_knockback.c:390" + }, + { + "name": "kickWaveCycles", + "value": 3, + "unit": "cycles over the kick", + "source": "src/main/PAL/main/car/car_ai.c:41" + }, + { + "name": "squatGearBase", + "value": 9, + "unit": "gear offset for the squat floor", + "source": "src/main/PAL/main/car/car_draw_and_knockback.c:166" + }, + { + "name": "brakeTiltSpeed", + "value": "0x51", + "unit": "speed units", + "source": "src/main/PAL/main/car/car_draw_and_knockback.c:177" + }, + { + "name": "tiltCap", + "value": 8, + "unit": "tilt units", + "source": "src/main/PAL/main/car/car_draw_and_knockback.c:182" + } + ], + "sources": [ + { + "path": "src/main/PAL/main/car/car_draw_and_knockback.c", + "line": 355 + }, + { + "path": "src/main/PAL/main/car/car_draw_and_knockback.c", + "line": 383 + }, + { + "path": "src/main/PAL/main/car/car_ai.c", + "line": 15 + }, + { + "path": "src/main/PAL/main/car/car_ai.c", + "line": 41 + }, + { + "path": "src/main/PAL/main/car/car_draw_and_knockback.c", + "line": 143 + }, + { + "path": "src/main/PAL/main/car/update_player_car.c", + "line": 306 + } + ], + "capabilities": [ + "impact shake", + "body pitch", + "weight transfer" + ], + "tags": [ + "feel", + "hit reaction", + "camera" + ] + }, + { + "id": "chase-camera", + "node": "camera", + "name": "Lagging chase camera", + "summary": "The chase view follows body yaw through an accelerating ramp with a speed-dependent damping and a step cap, then sits behind the car at one of three heights", + "behavior": "The chase view tracks the car yaw with a ramp rather than a fixed rate. Each update the yaw error picks a direction; the ramp grows by eight and the step is the square of the ramp over a damping value. Damping comes from 1250 minus speed: above 800 speed units it is lightly scaled and floored at six, and below that a quadratic in the same term is used, so the camera swings faster at speed. The step is capped at a seventeenth of the error times two, and never more than 64 angle units. Errors under five units reset the ramp. The eye is offset behind and above the car through its pitch and roll, then pulled back along one of three presets that pair a height with a distance. The view pitches down by a preset amount and the roll copies the body roll minus its roll velocity.", + "constants": [ + { + "name": "fastSpeedThreshold", + "value": "0x321", + "unit": "speed units", + "source": "src/main/PAL/main/track/update_camera.c:180" + }, + { + "name": "dampingBase", + "value": "0x4E2", + "unit": "speed units", + "source": "src/main/PAL/main/track/update_camera.c:181" + }, + { + "name": "rampStep", + "value": 8, + "unit": "ramp units, per update, rate unverified", + "source": "src/main/PAL/main/track/update_camera.c:204" + }, + { + "name": "stepCap", + "value": "0x40", + "unit": "angle units per update", + "source": "src/main/PAL/main/track/update_camera.c:198" + }, + { + "name": "nearPresetHeight", + "value": "0x3A", + "unit": "world units", + "source": "src/main/PAL/main/track/update_camera.c:325" + }, + { + "name": "nearPresetDistance", + "value": "0x118", + "unit": "world units", + "source": "src/main/PAL/main/track/update_camera.c:326" + }, + { + "name": "farPresetDistance", + "value": "0x190", + "unit": "world units", + "source": "src/main/PAL/main/track/update_camera.c:334" + } + ], + "sources": [ + { + "path": "src/main/PAL/main/track/update_camera.c", + "line": 160 + }, + { + "path": "src/main/PAL/main/track/update_camera.c", + "line": 186 + }, + { + "path": "src/main/PAL/main/track/update_camera.c", + "line": 201 + }, + { + "path": "src/main/PAL/main/track/update_camera.c", + "line": 323 + }, + { + "path": "src/main/PAL/main/track/update_camera.c", + "line": 345 + } + ], + "capabilities": [ + "third-person follow", + "yaw lag", + "camera presets" + ], + "tags": [ + "camera", + "follow", + "smoothing", + "distance" + ] + }, + { + "id": "track-side-cameras", + "node": "camera", + "name": "Track-side and path cameras", + "summary": "Replay and attract views pick the nearest track camera node, which either eases a fixed eye toward the car or interpolates a path between nodes", + "behavior": "Outside the two driver views, the camera mode comes from the nearest of a list of camera nodes placed along the track. A world-position node moves its eye toward a target point near the car by a node-specific blend in ten-thousandths each update. It then aims at the car with pitch and yaw from an arctangent, with no roll. A path node keeps a start value, a delta and a current value for a local offset and for pitch, yaw, roll and pull-back distance, and blends them over the node duration in frames. A change of node re-seeds the start from the current camera, so cuts inside a path stay smooth. The bumper view is the first mode: it rides a fixed offset of 448 sixteenth units along the body vertical axis and inherits the squat tilt.", + "constants": [ + { + "name": "blendScale", + "value": 10000, + "unit": "ten-thousandths per update", + "source": "src/main/PAL/main/track/update_camera.c:377" + }, + { + "name": "targetLift", + "value": "0x32", + "unit": "world units", + "source": "src/main/PAL/main/track/update_camera.c:370" + }, + { + "name": "bumperHeight", + "value": "-0x1C0", + "unit": "sixteenth world units along the body vertical axis", + "source": "src/main/PAL/main/track/update_camera.c:142" + } + ], + "sources": [ + { + "path": "src/main/PAL/main/track/update_camera.c", + "line": 119 + }, + { + "path": "src/main/PAL/main/track/update_camera.c", + "line": 356 + }, + { + "path": "src/main/PAL/main/track/update_camera.c", + "line": 377 + }, + { + "path": "src/main/PAL/main/track/update_camera.c", + "line": 394 + }, + { + "path": "src/main/PAL/main/track/update_camera.c", + "line": 130 + } + ], + "capabilities": [ + "track cameras", + "replay camera", + "bumper view", + "path interpolation" + ], + "tags": [ + "camera", + "replay", + "smoothing" + ] + }, + { + "id": "rival-speed-schedule", + "node": "rival-ai", + "name": "Rival speed keys and flicks", + "summary": "Each rival interpolates a target speed between per-course progress keys by its grid slot, and flicks its body in a slide when a key says so", + "behavior": "Each course direction has up to 48 speed keys at progress positions, each holding four target speed columns and a slide value. A rival finds the key pair around its progress and blends the two targets linearly. The four contenders each read their own column; the rest of the field reuses the fourth column scaled by 85 minus the car index, in percent, so later cars run slower. The blend becomes an acceleration limit through the inverse of the speedometer conversion, 1168 over 160, times six hundredths. A rival also carries a lateral racing-line offset that hint ranges along the track nudge, but only for the four contenders with no cars nearby. When a key carries a slide value, the rival gets a yaw input scaled by speed over 800. Its yaw rate then follows a decaying input, capped at plus or minus 700, which reads as a rival throwing the car into a corner.", + "constants": [ + { + "name": "fieldScaleBase", + "value": "0x55", + "unit": "percent", + "source": "src/main/PAL/main/car/car_ai.c:459" + }, + { + "name": "speedConversion", + "value": 1168, + "unit": "internal speed units per display conversion", + "source": "src/main/PAL/main/car/car_ai.c:483" + }, + { + "name": "accelerationShare", + "value": 6, + "unit": "hundredths", + "source": "src/main/PAL/main/car/car_ai.c:483" + }, + { + "name": "slideSpeedThreshold", + "value": "0x3C1", + "unit": "speed units", + "source": "src/main/PAL/main/car/car_ai.c:254" + }, + { + "name": "slideYawCap", + "value": "0x2BC", + "unit": "yaw units per update", + "source": "src/main/PAL/main/car/car_ai.c:290" + } + ], + "sources": [ + { + "path": "src/main/PAL/main/car/car_ai.c", + "line": 415 + }, + { + "path": "src/main/PAL/main/car/car_ai.c", + "line": 447 + }, + { + "path": "src/main/PAL/main/car/car_ai.c", + "line": 482 + }, + { + "path": "src/main/PAL/main/car/car_ai.c", + "line": 241 + }, + { + "path": "src/main/PAL/main/car/car_ai.c", + "line": 318 + }, + { + "path": "src/main/PAL/main/car/race_car_update.c", + "line": 68 + } + ], + "capabilities": [ + "ai speed profile", + "racing line", + "ai drift" + ], + "tags": [ + "ai", + "racing", + "opponent" + ] + }, + { + "id": "traffic-avoidance", + "node": "rival-ai", + "name": "Rival traffic avoidance", + "summary": "Rivals score the lanes ahead by how close blocking cars are and step sideways toward the emptiest lane, braking if boxed in", + "behavior": "Each update a rival looks at the other ten cars and the player. A car counts as ahead when its progress lead is positive and under a look-ahead of 3072 plus twice the rival speed; for the player the window is 6144 minus twice the player speed. Cars within 48 lateral units of the rival lane mark it blocked and add a closeness score to one of three lane buckets. The rival does not count a car ahead that is faster than it unless it is already avoiding. Very close cars on either side, under 512 progress units, mark that side unsafe. With anything ahead the rival targets 80 units left or right, picking the lower-scoring lane on a safe side, and steps six to eight units per update plus two while it avoids. A total score above 1000 cuts its acceleration limit to 30 percent. With nothing ahead it returns to its racing line.", + "constants": [ + { + "name": "lookAheadBase", + "value": "0xC00", + "unit": "progress units", + "source": "src/main/PAL/main/car/rival_ai.c:63" + }, + { + "name": "laneHalfWidth", + "value": "0x30", + "unit": "lateral units", + "source": "src/main/PAL/main/car/rival_ai.c:65" + }, + { + "name": "playerLookAhead", + "value": "0x1800", + "unit": "progress units", + "source": "src/main/PAL/main/car/rival_ai.c:102" + }, + { + "name": "avoidOffset", + "value": "-0x50", + "unit": "lateral units", + "source": "src/main/PAL/main/car/rival_ai.c:163" + }, + { + "name": "boxedInScore", + "value": "0x3E9", + "unit": "score units", + "source": "src/main/PAL/main/car/rival_ai.c:178" + }, + { + "name": "sideCheckRange", + "value": "0x200", + "unit": "progress units", + "source": "src/main/PAL/main/car/rival_ai.c:141" + } + ], + "sources": [ + { + "path": "src/main/PAL/main/car/rival_ai.c", + "line": 30 + }, + { + "path": "src/main/PAL/main/car/rival_ai.c", + "line": 119 + }, + { + "path": "src/main/PAL/main/car/rival_ai.c", + "line": 158 + }, + { + "path": "src/main/PAL/main/car/rival_ai.c", + "line": 178 + }, + { + "path": "src/main/PAL/main/car/rival_ai.c", + "line": 189 + }, + { + "path": "src/main/PAL/main/car/race_car_update.c", + "line": 54 + } + ], + "capabilities": [ + "ai avoidance", + "lane choice", + "overtaking" + ], + "tags": [ + "ai", + "racing", + "opponent", + "spatial test" + ] + }, + { + "id": "rubber-band", + "node": "rival-ai", + "name": "Contender rubber band", + "summary": "The four contenders are ranked each update, and whichever is ahead of the player by more than a course-dependent gap has its acceleration trimmed", + "behavior": "The four contender rivals are ranked by total progress. Starting from the last ranked, the first contender found ahead of the player is examined. If it leads by more than a far gap and is above 800 speed units, its acceleration limit drops to 90 percent. If it leads by more than a near gap and is above 1000 speed units, the limit drops to 98 percent. The gaps are 1536 and 3584 progress units on most courses and 3072 and 5120 on every course whose index is three modulo four. Separately, when a contender leads the next contender by more than 10240 progress units above 900 speed units, the leader loses fifteen percent of its limit. The same pass picks voice cues for close rivals and for taking the lead. The pass does nothing once the race reaches its late phase, and the field beyond the contenders runs avoidance only on alternate updates.", + "constants": [ + { + "name": "nearGap", + "value": "0x600", + "unit": "progress units", + "source": "src/main/PAL/main/car/rival_ai.c:296" + }, + { + "name": "farGap", + "value": "0xE00", + "unit": "progress units", + "source": "src/main/PAL/main/car/rival_ai.c:297" + }, + { + "name": "farGapTrim", + "value": 90, + "unit": "percent of acceleration limit kept", + "source": "src/main/PAL/main/car/rival_ai.c:320" + }, + { + "name": "nearGapTrim", + "value": 98, + "unit": "percent of acceleration limit kept", + "source": "src/main/PAL/main/car/rival_ai.c:328" + }, + { + "name": "leaderSpreadGap", + "value": "0x2800", + "unit": "progress units", + "source": "src/main/PAL/main/car/rival_ai.c:206" + }, + { + "name": "leaderSpreadSpeed", + "value": "0x385", + "unit": "speed units", + "source": "src/main/PAL/main/car/rival_ai.c:210" + } + ], + "sources": [ + { + "path": "src/main/PAL/main/car/rival_ai.c", + "line": 223 + }, + { + "path": "src/main/PAL/main/car/rival_ai.c", + "line": 282 + }, + { + "path": "src/main/PAL/main/car/rival_ai.c", + "line": 292 + }, + { + "path": "src/main/PAL/main/car/rival_ai.c", + "line": 317 + }, + { + "path": "src/main/PAL/main/car/rival_ai.c", + "line": 192 + }, + { + "path": "src/main/PAL/main/car/race_car_update.c", + "line": 75 + } + ], + "capabilities": [ + "rubber banding", + "ai ranking", + "difficulty balance" + ], + "tags": [ + "ai", + "racing", + "opponent", + "balance" + ] + } + ] +} diff --git a/data/games/richard-burns-rally.json b/data/games/richard-burns-rally.json new file mode 100644 index 0000000..2f8c5eb --- /dev/null +++ b/data/games/richard-burns-rally.json @@ -0,0 +1,1565 @@ +{ + "version": 1, + "id": "richard-burns-rally", + "title": "Richard Burns Rally", + "repo": "https://github.com/JodusNodus/rerbr", + "sha": "6ebd7b820291873226ccca746e42a1b7cd2003da", + "nodes": [ + { + "id": "frame-clock", + "name": "Frame clock and substep pacing", + "purpose": "Turns each frame's wall time into whole physics substeps, clamps an overlong frame, keeps the leftover time and settles the car at load", + "paths": [ + "crates/rbr-gameplay/src/tick.rs", + "crates/rbr-gameplay/src/game.rs", + "crates/rbr-recording/src/lib.rs", + "crates/rbr-physics/src/physics_core/step.rs" + ], + "functions": [ + { + "name": "step_interval", + "path": "crates/rbr-gameplay/src/tick.rs", + "lines": [ + 20, + 26 + ] + }, + { + "name": "frame", + "path": "crates/rbr-gameplay/src/tick.rs", + "lines": [ + 54, + 60 + ] + }, + { + "name": "frame", + "path": "crates/rbr-gameplay/src/game.rs", + "lines": [ + 350, + 616 + ] + } + ], + "dependsOn": [] + }, + { + "id": "physics-step", + "name": "Physics step dispatch", + "purpose": "Orders force input, acceleration and integration over every body, runs the contact and solver passes and advances the physics clock once per substep", + "paths": [ + "crates/rbr-physics/src/physics_core/step.rs", + "crates/rbr-physics/src/physics_core/pacing.rs" + ], + "functions": [ + { + "name": "step_forward", + "path": "crates/rbr-physics/src/physics_core/step.rs", + "lines": [ + 50, + 173 + ] + }, + { + "name": "step_bodies", + "path": "crates/rbr-physics/src/physics_core/step.rs", + "lines": [ + 28, + 35 + ] + }, + { + "name": "advance_time", + "path": "crates/rbr-physics/src/physics_core/pacing.rs", + "lines": [ + 16, + 21 + ] + } + ], + "dependsOn": [ + "frame-clock" + ] + }, + { + "id": "suspension", + "name": "Spring-damper struts", + "purpose": "Per-wheel spring, helper spring, bump and droop stops, damper fade and the high-speed damping that takes over at break speeds", + "paths": [ + "crates/rbr-physics/src/spring_damper/mod.rs", + "crates/rbr-physics/src/spring_damper/suspension.rs", + "crates/rbr-physics/src/ngp/spring.rs", + "crates/rbr-physics/src/wheel/strut.rs" + ], + "functions": [ + { + "name": "step", + "path": "crates/rbr-physics/src/spring_damper/mod.rs", + "lines": [ + 462, + 559 + ] + }, + { + "name": "update_forces", + "path": "crates/rbr-physics/src/spring_damper/mod.rs", + "lines": [ + 444, + 450 + ] + }, + { + "name": "bump_stop", + "path": "crates/rbr-physics/src/ngp/spring.rs", + "lines": [ + 43, + 71 + ] + }, + { + "name": "latch", + "path": "crates/rbr-physics/src/ngp/spring.rs", + "lines": [ + 96, + 104 + ] + }, + { + "name": "rest_travel", + "path": "crates/rbr-physics/src/wheel/strut.rs", + "lines": [ + 11, + 13 + ] + } + ], + "dependsOn": [ + "physics-step" + ] + }, + { + "id": "tyre", + "name": "Tyre contact, friction and heat", + "purpose": "The contact patch on the road, the slip curve that turns slip into friction, the low-speed hold, the ploughing of loose ground and the heat and wear the tyre takes", + "paths": [ + "crates/rbr-physics/src/tyre/friction_curve.rs", + "crates/rbr-physics/src/tyre/grip.rs", + "crates/rbr-physics/src/tyre/contact_patch.rs", + "crates/rbr-physics/src/tyre/heat.rs", + "crates/rbr-physics/src/tyre/hold.rs", + "crates/rbr-physics/src/tyre/soil.rs" + ], + "functions": [ + { + "name": "friction_curve", + "path": "crates/rbr-physics/src/tyre/friction_curve.rs", + "lines": [ + 84, + 121 + ] + }, + { + "name": "limit_to_friction_circle", + "path": "crates/rbr-physics/src/tyre/grip.rs", + "lines": [ + 74, + 99 + ] + }, + { + "name": "contact_patch", + "path": "crates/rbr-physics/src/tyre/contact_patch.rs", + "lines": [ + 27, + 161 + ] + }, + { + "name": "heat_step", + "path": "crates/rbr-physics/src/tyre/heat.rs", + "lines": [ + 92, + 186 + ] + }, + { + "name": "hold", + "path": "crates/rbr-physics/src/tyre/hold.rs", + "lines": [ + 57, + 136 + ] + } + ], + "dependsOn": [ + "physics-step", + "suspension" + ] + }, + { + "id": "stage-world", + "name": "Stage collision world and surface maps", + "purpose": "Builds the stage's physics world from its collision and material files: the triangle meshes in a cell grid, the material maps chosen by tyre wear and wetness, and the brake wall", + "paths": [ + "crates/rbr-physics/src/stage/mod.rs", + "crates/rbr-physics/src/physics_core/detect.rs", + "crates/rbr-physics/src/collision/mesh_hit.rs", + "crates/rbr-physics/src/collision/mesh_grid.rs" + ], + "functions": [ + { + "name": "select_maps", + "path": "crates/rbr-physics/src/stage/mod.rs", + "lines": [ + 117, + 137 + ] + }, + { + "name": "col_root", + "path": "crates/rbr-physics/src/stage/mod.rs", + "lines": [ + 56, + 66 + ] + }, + { + "name": "channel_sum", + "path": "crates/rbr-physics/src/collision/mesh_hit.rs", + "lines": [ + 30, + 33 + ] + }, + { + "name": "grid_bounds", + "path": "crates/rbr-physics/src/physics_core/detect.rs", + "lines": [ + 85, + 90 + ] + } + ], + "dependsOn": [ + "physics-step" + ] + }, + { + "id": "car-body", + "name": "Car body, impacts and brake walls", + "purpose": "The car's own body: the damage handler that records impact damage zone by zone, the drivability gate and the friction a car takes while it is pressed into a brake wall", + "paths": [ + "crates/rbr-physics/src/car_body/damage.rs", + "crates/rbr-physics/src/car_body/wall_friction.rs", + "crates/rbr-physics/src/car_body/forces.rs" + ], + "functions": [ + { + "name": "impact_scale", + "path": "crates/rbr-physics/src/car_body/damage.rs", + "lines": [ + 145, + 147 + ] + }, + { + "name": "set_level", + "path": "crates/rbr-physics/src/car_body/damage.rs", + "lines": [ + 13, + 23 + ] + }, + { + "name": "wall_friction", + "path": "crates/rbr-physics/src/car_body/wall_friction.rs", + "lines": [ + 60, + 99 + ] + } + ], + "dependsOn": [ + "physics-step", + "stage-world" + ] + }, + { + "id": "driveline", + "name": "Engine, clutch and gearbox logic", + "purpose": "The gearbox's own decisions: launch and rpm shift requests, the shift lockout, the downshift slip gate and the differential locks the drive is given", + "paths": [ + "crates/rbr-physics/src/vcu/gear.rs", + "crates/rbr-physics/src/vcu/slip.rs", + "crates/rbr-physics/src/vcu/locks.rs", + "crates/rbr-physics/src/engine_driveline/drive_step.rs" + ], + "functions": [ + { + "name": "update", + "path": "crates/rbr-physics/src/vcu/gear.rs", + "lines": [ + 35, + 75 + ] + }, + { + "name": "query_slip", + "path": "crates/rbr-physics/src/vcu/slip.rs", + "lines": [ + 15, + 39 + ] + }, + { + "name": "diff_locks", + "path": "crates/rbr-physics/src/vcu/locks.rs", + "lines": [ + 31, + 44 + ] + } + ], + "dependsOn": [ + "physics-step" + ] + }, + { + "id": "controls", + "name": "Driver input payload and force feedback", + "purpose": "Quantises the filtered pedals and steering into the fixed control payload the physics reads, maps the shift and ignition actions, and turns the steering force into a force feedback magnitude", + "paths": [ + "crates/rbr-gameplay/src/control.rs", + "crates/rbr-gameplay/src/ffb.rs", + "crates/rbr-gameplay/src/filter.rs" + ], + "functions": [ + { + "name": "pack", + "path": "crates/rbr-gameplay/src/control.rs", + "lines": [ + 133, + 148 + ] + }, + { + "name": "ngp_direct_gears", + "path": "crates/rbr-gameplay/src/control.rs", + "lines": [ + 200, + 213 + ] + }, + { + "name": "magnitude", + "path": "crates/rbr-gameplay/src/ffb.rs", + "lines": [ + 41, + 51 + ] + } + ], + "dependsOn": [ + "frame-clock" + ] + }, + { + "id": "pace-notes", + "name": "Marker ticker and pace notes", + "purpose": "Advances the stage's marker keys against the car's distance, fires start, split and finish events and calls pace notes out ahead of the car", + "paths": [ + "crates/rbr-gameplay/src/markers.rs", + "crates/rbr-formats/src/markers.rs" + ], + "functions": [ + { + "name": "tick", + "path": "crates/rbr-gameplay/src/markers.rs", + "lines": [ + 288, + 359 + ] + }, + { + "name": "pace_stack", + "path": "crates/rbr-gameplay/src/markers.rs", + "lines": [ + 373, + 381 + ] + }, + { + "name": "spoken_tag", + "path": "crates/rbr-gameplay/src/markers.rs", + "lines": [ + 363, + 365 + ] + } + ], + "dependsOn": [ + "frame-clock" + ] + }, + { + "id": "stage-timing", + "name": "Stage time card and race clock", + "purpose": "Finds the start, marks splits and the finish against the car's distance and elapsed time, keeps the stage clock and the countdown, and reports a still or moving start", + "paths": [ + "crates/rbr-gameplay/src/timing.rs", + "crates/rbr-gameplay/src/race_clock.rs" + ], + "functions": [ + { + "name": "record", + "path": "crates/rbr-gameplay/src/timing.rs", + "lines": [ + 269, + 273 + ] + }, + { + "name": "update", + "path": "crates/rbr-gameplay/src/timing.rs", + "lines": [ + 277, + 290 + ] + }, + { + "name": "tick", + "path": "crates/rbr-gameplay/src/race_clock.rs", + "lines": [ + 216, + 279 + ] + } + ], + "dependsOn": [ + "frame-clock", + "pace-notes" + ] + }, + { + "id": "weather", + "name": "Weather, season and conditions", + "purpose": "Reads the season's per-stage weather rows, picks the track letter, cloud, rain preset, wetness and tyre wear, sets the time of day and selects the wet collision meshes and the tyre kind", + "paths": [ + "crates/rbr-gameplay/src/weather.rs", + "crates/rbr-formats/src/rsf/mod.rs" + ], + "functions": [ + { + "name": "start_clock", + "path": "crates/rbr-gameplay/src/weather.rs", + "lines": [ + 402, + 410 + ] + }, + { + "name": "wet_meshes", + "path": "crates/rbr-gameplay/src/weather.rs", + "lines": [ + 340, + 342 + ] + }, + { + "name": "tyre_kind", + "path": "crates/rbr-gameplay/src/weather.rs", + "lines": [ + 304, + 311 + ] + } + ], + "dependsOn": [ + "stage-timing" + ] + }, + { + "id": "service-damage", + "name": "Carried damage and the service park", + "purpose": "Reads the damage the car carries back from a stage as a level per part, repairs parts on order, and runs the service budget, the repair clock and the late penalty", + "paths": [ + "crates/rbr-gameplay/src/car_damage.rs", + "crates/rbr-gameplay/src/service/mod.rs", + "crates/rbr-gameplay/src/service/tree.rs" + ], + "functions": [ + { + "name": "severity", + "path": "crates/rbr-gameplay/src/car_damage.rs", + "lines": [ + 288, + 360 + ] + }, + { + "name": "repair", + "path": "crates/rbr-gameplay/src/car_damage.rs", + "lines": [ + 410, + 482 + ] + }, + { + "name": "late_penalty", + "path": "crates/rbr-gameplay/src/service/mod.rs", + "lines": [ + 129, + 137 + ] + }, + { + "name": "tick", + "path": "crates/rbr-gameplay/src/service/mod.rs", + "lines": [ + 473, + 500 + ] + } + ], + "dependsOn": [ + "car-body" + ] + }, + { + "id": "camera", + "name": "Driving cameras", + "purpose": "Places the chase and bonnet eyes on the car, lags and pushes them out of the stage's geometry, and builds the start orbit and the field of view", + "paths": [ + "crates/rbr-render/src/game_camera.rs", + "crates/rbr-render/src/coords.rs", + "crates/rbr-render/src/camera.rs" + ], + "functions": [ + { + "name": "fov", + "path": "crates/rbr-render/src/game_camera.rs", + "lines": [ + 186, + 188 + ] + }, + { + "name": "bonnet_lift", + "path": "crates/rbr-render/src/game_camera.rs", + "lines": [ + 192, + 198 + ] + }, + { + "name": "orbit_reach", + "path": "crates/rbr-render/src/game_camera.rs", + "lines": [ + 203, + 217 + ] + } + ], + "dependsOn": [] + } + ], + "cards": [ + { + "id": "fixed-substep-clock", + "node": "frame-clock", + "name": "Fixed substep clock", + "summary": "Wall time is cut into identical physics substeps so that handling, timing and suspension all advance by the same 7 ms slice.", + "behavior": "Each frame the loop measures the time since the previous frame, caps a long gap at a tenth of a second and drops the excess, then takes as many whole physics substeps as fit and carries the remainder into the next frame. In the retail game a substep is seven milliseconds; the recording layer keeps that value as the canonical step for every recorded drive, while a live player's steps under the community physics build run at a finer rate. Before the first frame the car is settled with four calls of forty substeps, which is why recorded drives open with a burst of identical frames. The contact and solver passes are not run on every substep: the retail build ran them on every fourth count, and the community physics build patches that to four of every thirty-two with a collision window of twenty substeps in place of the retail four. The frame split, the settle burst and the step dispatch are reversed from the retail executable's own loop and its physics entry point, not rewritten; the Rust side reproduces the original's operation order so a drive can be replayed bit for bit.", + "constants": [ + { + "name": "Longest wall time a frame may count", + "value": 0.1, + "unit": "seconds of wall time per frame; the cap applied before whole 7 ms physics substeps are taken", + "source": "crates/rbr-gameplay/src/tick.rs:11" + }, + { + "name": "Physics substep length", + "value": 0.007, + "unit": "seconds per physics substep (7 ms); one substep is one integration step for every body", + "source": "crates/rbr-recording/src/lib.rs:55" + }, + { + "name": "Load settle substeps", + "value": 40, + "unit": "substeps per load-settle call; four such calls run before the first frame, at 7 ms per substep", + "source": "crates/rbr-gameplay/src/tick.rs:16" + }, + { + "name": "Collision window", + "value": 20, + "unit": "physics substeps covered by one collision window, evaluated once per 7 ms substep", + "source": "crates/rbr-physics/src/physics_core/step.rs:11" + } + ], + "sources": [ + { + "path": "crates/rbr-gameplay/src/tick.rs", + "line": 54 + }, + { + "path": "crates/rbr-recording/src/lib.rs", + "line": 55 + }, + { + "path": "crates/rbr-physics/src/physics_core/step.rs", + "line": 50 + } + ], + "capabilities": [ + "fixed timestep", + "frame pacing", + "load settle" + ], + "tags": [ + "physics", + "timing", + "engine" + ] + }, + { + "id": "strut-travel-and-bump-rubber", + "node": "suspension", + "name": "Strut travel and bump rubber", + "summary": "An independent suspension corner is a spring with helper, two stops and a rubber that engages near full compression and softens the corner for that calculation alone.", + "behavior": "Travel is measured between a bump stop position and a droop stop position. The height at rest is a weighted blend of the two stop positions, seven tenths from the droop side and a third from the bump side, so the strut sits close to the middle of its travel when the car is loaded. When the corner compresses into the last five centimetres before the bump stop, less whatever gap the wheel's settings subtract, a rubber stop engages: its engagement ratio is the depth over the gap when the progressive option is on and full engagement otherwise, its stiffness is scaled by that ratio and its damping by the ratio's square root, so a grazing hit on a crest is much softer than a full compression. Compression divides that damping further by an integer divisor, and a positive rebound ratio multiplies it, which is how a corner can be made stiff in one direction only. The rubber's values replace the spring and damper definition for one calculation, and the original definition is restored afterwards. The whole strut, its rubber and its rest travel are ports of the retail executable's suspension code with the community physics build's changes layered on; the customer of the port is the memory image, so the arithmetic is reversed rather than rewritten.", + "constants": [ + { + "name": "Rubber engagement gap", + "value": 0.05, + "unit": "metres of strut travel before the bump stop, over which the rubber ramps in; read once per strut step at the 7 ms physics substep", + "source": "crates/rbr-physics/src/ngp/spring.rs:54" + }, + { + "name": "Compression damping divisor", + "value": 0.3, + "unit": "dimensionless factor dividing the rubber's damping in compression; applied once per strut step", + "source": "crates/rbr-physics/src/ngp/spring.rs:64" + }, + { + "name": "Droop weight of the rest travel", + "value": "0.669_999_96", + "unit": "dimensionless weight of the droop stop in the rest position; evaluated when a setup is adopted, not per step", + "source": "crates/rbr-physics/src/wheel/strut.rs:4" + }, + { + "name": "Bump weight of the rest travel", + "value": 0.33, + "unit": "dimensionless weight of the bump stop in the rest position; evaluated when a setup is adopted, not per step", + "source": "crates/rbr-physics/src/wheel/strut.rs:6" + }, + { + "name": "Droop stop rate of a fresh strut", + "value": "600_000.0", + "unit": "spring rate for the droop stop, per metre of travel; set once when the strut's definition is built", + "source": "crates/rbr-physics/src/spring_damper/mod.rs:58" + } + ], + "sources": [ + { + "path": "crates/rbr-physics/src/ngp/spring.rs", + "line": 43 + }, + { + "path": "crates/rbr-physics/src/wheel/strut.rs", + "line": 11 + }, + { + "path": "crates/rbr-physics/src/spring_damper/mod.rs", + "line": 462 + } + ], + "capabilities": [ + "independent suspension", + "progressive bump stop", + "droop stop", + "helper spring" + ], + "tags": [ + "physics", + "suspension", + "handling" + ] + }, + { + "id": "damper-fade-and-high-speed-latch", + "node": "suspension", + "name": "Damper fade and the high-speed latch", + "summary": "Sustained suspension movement fades the damper, a separate high-speed valve takes over past break speeds, and bump damping never falls below a floor.", + "behavior": "The strut carries a fade amount that grows while the damper works hard. At each update the bump damping is reduced in proportion to the fade, the rebound damping fades less, and the high-speed bump damping fades more than either, with a floor under the bump value so a fully faded corner still resists. Separately, the high-speed rebound damping is scaled by one minus a fraction of the fade, so a tired damper loses some of its fast-rebound control. During a step the damper compares the strut velocity with the two break speeds: past the bump break speed the high-speed bump value becomes the effective damping and the break speed is cleared for that step; past the rebound break speed the scaled high-speed rebound value does the same. The latch is held for a short run of steps so the valve does not chatter, and the pre-step values are kept on the strut and on the car for the rest of the frame. The fade weights and the effective damping update are ports of the retail executable's damper update; the break-speed wrapper belongs to the community physics build and is reversed from that build, not from the retail game.", + "constants": [ + { + "name": "Bump fade weight", + "value": 0.7, + "unit": "fraction of the fade amount removed from bump damping; applied once per strut update at the 7 ms substep", + "source": "crates/rbr-physics/src/spring_damper/mod.rs:446" + }, + { + "name": "Rebound fade weight", + "value": 0.3, + "unit": "fraction of the fade amount removed from rebound damping; applied once per strut update at the 7 ms substep", + "source": "crates/rbr-physics/src/spring_damper/mod.rs:448" + }, + { + "name": "High-speed bump fade weight", + "value": 0.8, + "unit": "fraction of the fade amount removed from high-speed bump damping; applied once per strut update at the 7 ms substep", + "source": "crates/rbr-physics/src/spring_damper/mod.rs:449" + }, + { + "name": "Minimum bump damping", + "value": 400, + "unit": "damping force per metre per second, the floor under the faded bump value; enforced once per strut update at the 7 ms substep", + "source": "crates/rbr-physics/src/spring_damper/mod.rs:21" + }, + { + "name": "High-speed rebound fade share", + "value": 0.4, + "unit": "dimensionless share of the fade that scales high-speed rebound damping; read once per strut step at the 7 ms substep", + "source": "crates/rbr-physics/src/ngp/spring.rs:76" + } + ], + "sources": [ + { + "path": "crates/rbr-physics/src/spring_damper/mod.rs", + "line": 444 + }, + { + "path": "crates/rbr-physics/src/ngp/spring.rs", + "line": 96 + } + ], + "capabilities": [ + "damper fade", + "high-speed damping", + "bump and rebound split" + ], + "tags": [ + "physics", + "suspension", + "handling" + ] + }, + { + "id": "tyre-slip-curve", + "node": "tyre", + "name": "Tyre slip curve and friction circle", + "summary": "Slip becomes friction through a curve that peaks and falls to a sliding value, while the carcass forces are held inside a circle set by the load and the surface coefficient.", + "behavior": "The curve rises from zero along a shape whose knee is either three times the slip at which the linear stiffness would reach peak friction or the peak slip itself, whichever is smaller. Up to the knee the friction follows a cubic that passes through the peak; beyond it the falloff is read from a small table and blended down to the sliding friction at the end slip, and past that the friction stays flat while the pneumatic trail goes to zero. The peak is reached at a slip derived from peak friction, load and slip stiffness, so a heavier or softer tyre peaks later. In parallel, the lateral deflection and torsion of the carcass are treated as spring forces and are clamped to a circle whose radius is the normal force times the coefficient used, static while the state is clamped and kinetic otherwise; when the forces exceed the circle both are scaled back together, which is what makes a tyre slide rather than grip past its limit. At walking pace a hold torque keeps the contact direction from spinning freely, capped at a share of the normal force and with the slip speed clamped to a range. All of this is reversed from the retail executable's tyre solver, with NaN behaviour and operation order copied from its assembly rather than from any C++ reference.", + "constants": [ + { + "name": "Peak-friction slip factor", + "value": 1.2, + "unit": "dimensionless multiplier on the slip at which the linear stiffness reaches peak friction; evaluated once per tyre step at the 7 ms substep", + "source": "crates/rbr-physics/src/tyre/friction_curve.rs:87" + }, + { + "name": "Knee tripling", + "value": 3, + "unit": "dimensionless factor applied to the linear slip when it falls under the peak slip; evaluated once per tyre step", + "source": "crates/rbr-physics/src/tyre/friction_curve.rs:92" + }, + { + "name": "Stiffness treated as zero", + "value": 0.001, + "unit": "stiffness units below which a carcass spring is treated as absent; tested once per clamp at the 7 ms substep", + "source": "crates/rbr-physics/src/tyre/grip.rs:13" + }, + { + "name": "Low-speed hold grip cap", + "value": 0.3, + "unit": "share of the normal force times kinetic friction that the hold torque may use; applied once per tyre step", + "source": "crates/rbr-physics/src/tyre/hold.rs:15" + }, + { + "name": "Hold slip-speed ceiling", + "value": 100, + "unit": "metres per second, the top of the range the hold's slip speed is clamped to; applied once per tyre step", + "source": "crates/rbr-physics/src/tyre/hold.rs:18" + } + ], + "sources": [ + { + "path": "crates/rbr-physics/src/tyre/friction_curve.rs", + "line": 84 + }, + { + "path": "crates/rbr-physics/src/tyre/grip.rs", + "line": 74 + }, + { + "path": "crates/rbr-physics/src/tyre/hold.rs", + "line": 57 + } + ], + "capabilities": [ + "slip curve", + "friction circle", + "pneumatic trail", + "low-speed hold" + ], + "tags": [ + "physics", + "tyres", + "grip" + ] + }, + { + "id": "tyre-heat-and-wear", + "node": "tyre", + "name": "Tyre heat and wear", + "summary": "Friction power heats the tread in layers, the surface sheds heat to the road and the air, and the rubber wears only once it is sliding.", + "behavior": "Each tyre step turns the forces at the contact point into heat: rolling resistance contributes half its power, the lateral and longitudinal slip forces contribute the rest. That heat is split between the surface layer, the tread beneath it and the carcass, with some of it passed down and a little returned; the surface loses heat to the road at a rate that depends on how hot it is and on how wet the surface is, since water carries heat away faster. The tread counts as hot only above a threshold, and the difference above it is ramped to a fraction between zero and one that scales the conductance. Wear is gated: while the squared slip velocity stays small the abrasion rate is zero, and once the tyre is sliding, the amount past the peak of the slip curve decides how much rubber is taken. The step's heat and wear coefficients belong to the community physics build, which replaced the retail tyre's thermal values; the structure of the step and the layer split are reversed from that build rather than rewritten.", + "constants": [ + { + "name": "Hot tread threshold", + "value": 40, + "unit": "degrees Celsius above which the tread counts as hot; the difference is taken once per tyre step at the 7 ms substep", + "source": "crates/rbr-physics/src/tyre/heat.rs:13" + }, + { + "name": "Hotness ramp", + "value": 0.0125, + "unit": "fraction of hotness per kelvin above the threshold, clamped to one; computed once per tyre step", + "source": "crates/rbr-physics/src/tyre/heat.rs:112" + }, + { + "name": "Slip-squared wear gate", + "value": 0.4, + "unit": "squared metres per second squared, the slip velocity below which no wear is taken; tested once per tyre step", + "source": "crates/rbr-physics/src/tyre/heat.rs:163" + }, + { + "name": "Minimum abrasion rate", + "value": 0.05, + "unit": "wear units per second, below which the rate is reported as zero; applied once per tyre step", + "source": "crates/rbr-physics/src/tyre/heat.rs:182" + } + ], + "sources": [ + { + "path": "crates/rbr-physics/src/tyre/heat.rs", + "line": 92 + }, + { + "path": "crates/rbr-physics/src/tyre/contact_patch.rs", + "line": 27 + } + ], + "capabilities": [ + "tyre temperature", + "tread wear", + "wet cooling" + ], + "tags": [ + "physics", + "tyres", + "weather" + ] + }, + { + "id": "loose-surface-ploughing", + "node": "tyre", + "name": "Loose surface ploughing", + "summary": "On gravel, snow and mud a sliding tyre pushes soil aside, and the mass it moves becomes a drag that pulls the car toward the slide and slows it.", + "behavior": "The soil the tyre meets is described by how deep it sinks, how much the carcass compresses, the tread coverage and the carcass width. From those the step builds the side area the lateral motion sweeps and the section the longitudinal motion pushes, converts both to a mass flow through the soil's density, and makes a drag force out of the contact point's lateral velocity times that flow. Two flow rates are reported, one for a longitudinal slip that fights the direction of travel and one for a slip that goes with it, which is what makes a locked wheel dig in differently from a spinning one. A furrow ratio compares the swept side area with twice the effective radius and is blended with the lateral share of the force, so a wide, sunk tyre drags more of its own contact patch. The results are smoothed against the previous step, keeping nine tenths of the old average and taking a tenth of the new value, which is why the drag builds and releases over a few frames instead of snapping. The original also multiplies two of the terms by zero and the port keeps those products, so the longitudinal force stays zero: the whole function is reversed from the retail executable's tyre code.", + "constants": [ + { + "name": "Soil drag smoothing, kept share", + "value": 0.9, + "unit": "fraction of the previous average kept; applied once per tyre step at the 7 ms substep", + "source": "crates/rbr-physics/src/tyre/soil.rs:4" + }, + { + "name": "Soil drag smoothing, new share", + "value": 0.1, + "unit": "fraction of the new value taken into the average; applied once per tyre step at the 7 ms substep", + "source": "crates/rbr-physics/src/tyre/soil.rs:5" + }, + { + "name": "Furrow radius factor", + "value": 2, + "unit": "dimensionless factor on the effective radius in the furrow ratio; evaluated once per tyre step", + "source": "crates/rbr-physics/src/tyre/soil.rs:76" + } + ], + "sources": [ + { + "path": "crates/rbr-physics/src/tyre/soil.rs", + "line": 62 + }, + { + "path": "crates/rbr-physics/src/tyre/contact_patch.rs", + "line": 27 + } + ], + "capabilities": [ + "loose surface drag", + "soil displacement", + "gravel and snow handling" + ], + "tags": [ + "physics", + "tyres", + "surfaces" + ] + }, + { + "id": "stage-collision-grid", + "node": "stage-world", + "name": "Stage collision grid and surface sampling", + "summary": "The stage's triangle meshes sit in a cell grid that is queried around the car; each hit face carries packed vertex slots and material weights that are blended across the triangle.", + "behavior": "The stage's collision file becomes a set of meshes, each built into boxes over a cell grid, with a brake wall and optional wet and water mesh lists behind the same root. A query starts from the cells under the car box's four projected corners; when the box is widened the lookup uses one margin either side of the box's midpoint on each axis instead, which is what catches a thin post or a fence rail that the corners miss. An edge sweep keeps the nearest hit below a starting distance and can stop early. With no brake wall to bound it, the grid falls back to a fixed extent and origin that covers the retail stages, so a stage with no wall data still has a sane world. Each mesh face is a compact record of three vertex slots and two sets of per-corner bytes: the low nibble weights are used where the original wanted a fine blend, the five-bit channels where it wanted a coarse one, and both are summed with the barycentric coordinates of the hit to give the surface properties at that exact point. The face-record layout, the cell grid and the query walk are reversed from the retail executable's collision system; the surface reads keep the original's packed-byte arithmetic.", + "constants": [ + { + "name": "Widened lookup margin", + "value": 4.7, + "unit": "metres added either side of the box midpoint on each ground axis; used per query at the 7 ms physics substep", + "source": "crates/rbr-physics/src/physics_core/detect.rs:7" + }, + { + "name": "Edge sweep starting distance", + "value": 1.01, + "unit": "metres, the initial best distance an edge-pair sweep must beat; used per sweep at the 7 ms substep", + "source": "crates/rbr-physics/src/physics_core/detect.rs:9" + }, + { + "name": "Fallback grid width", + "value": 10000, + "unit": "metres across the stage in x when there is no brake wall to bound the grid; set once at stage load", + "source": "crates/rbr-physics/src/physics_core/detect.rs:80" + }, + { + "name": "Fallback grid height", + "value": 15000, + "unit": "metres across the stage in y when there is no brake wall to bound the grid; set once at stage load", + "source": "crates/rbr-physics/src/physics_core/detect.rs:81" + }, + { + "name": "Mesh box plane margin", + "value": 0.1, + "unit": "metres of margin on the plane when a mesh box is inserted into the grid; built once at stage load", + "source": "crates/rbr-physics/src/collision/mesh_grid.rs:21" + }, + { + "name": "Crease cosine", + "value": "0.984_807_75", + "unit": "dimensionless cosine of a ten degree crease, the edge filter's bound; evaluated once per mesh build", + "source": "crates/rbr-physics/src/collision/mesh_grid.rs:23" + } + ], + "sources": [ + { + "path": "crates/rbr-physics/src/collision/mesh_hit.rs", + "line": 30 + }, + { + "path": "crates/rbr-physics/src/stage/mod.rs", + "line": 56 + }, + { + "path": "crates/rbr-physics/src/physics_core/detect.rs", + "line": 85 + } + ], + "capabilities": [ + "collision meshes", + "cell grid queries", + "surface material blending", + "wet meshes" + ], + "tags": [ + "physics", + "collision", + "stage" + ] + }, + { + "id": "impact-damage", + "node": "car-body", + "name": "Impact damage", + "summary": "A hit is turned into damage zone by zone, the car keeps driving until enough zones fail, and the damage level changes how much of a hit is recorded.", + "behavior": "An impact's length is scaled before it reaches the car's damage handler, and the handler walks a set of functors, each with its own random spread, to decide which zones the hit reaches and how hard. A zone's stored float only ever rises, is capped at one, and a zone index outside the recorded range is ignored. Four damage levels exist for the whole car and each carries its own threshold: an off setting records nothing, a reduced setting records only beyond a fifth of the scale, the normal setting beyond seven tenths, and the harshest beyond one. Separately, the car's drivability gate turns on four counters and a windshield level: a car whose windshield value reaches zero or whose counters mostly read zero is no longer drivable, which is how a wrecked car ends a stage instead of limping on. The impact path also rolls a random draw for engine damage and fires only on the low tail of that draw, and keeps a short ring of recent engine events. Every one of these branches is reversed from the retail executable's damage handler, including its random generator and the exact rounding of each drawn value; the damage-level option itself is reversed from the game's own options code.", + "constants": [ + { + "name": "Impact length scale", + "value": 0.08, + "unit": "damage units per metre of impact length; applied once per impact event", + "source": "crates/rbr-physics/src/car_body/damage.rs:146" + }, + { + "name": "Reduced damage threshold", + "value": 0.2, + "unit": "fraction of the damage scale a hit must reach at the reduced level; read once per impact", + "source": "crates/rbr-physics/src/car_body/damage.rs:18" + }, + { + "name": "Normal damage threshold", + "value": 0.7, + "unit": "fraction of the damage scale a hit must reach at the normal level; read once per impact", + "source": "crates/rbr-physics/src/car_body/damage.rs:19" + }, + { + "name": "Harshest damage threshold", + "value": 1, + "unit": "fraction of the damage scale a hit must reach at the harshest level; read once per impact", + "source": "crates/rbr-physics/src/car_body/damage.rs:20" + }, + { + "name": "Engine fire draw", + "value": 0.05, + "unit": "fraction of the draw range below which engine damage catches fire; rolled once per engine damage gate", + "source": "crates/rbr-physics/src/car_body/damage.rs:204" + } + ], + "sources": [ + { + "path": "crates/rbr-physics/src/car_body/damage.rs", + "line": 145 + }, + { + "path": "crates/rbr-physics/src/car_body/damage.rs", + "line": 13 + }, + { + "path": "crates/rbr-gameplay/src/car_damage.rs", + "line": 288 + } + ], + "capabilities": [ + "impact damage", + "damage levels", + "drivability gate", + "engine fire" + ], + "tags": [ + "physics", + "damage", + "handling" + ] + }, + { + "id": "brake-wall-friction", + "node": "car-body", + "name": "Brake wall friction", + "summary": "A car pressed into a wall or a snow bank is held back by a friction force that grows with penetration and speed, with a low-speed and a high-speed term and a yaw torque.", + "behavior": "Once the car's penetration into a brake wall is positive, the force accumulator minus the car's weight gives a support force; if the support points upward and the car is moving sideways, a force opposing the velocity is added, scaled by the penetration, by a low-speed term that saturates after half a metre per second and by a high-speed term with a much smaller gain. The two terms are mixed with fixed shares and the result is applied at a lever arm below the car's centre, so scraping a wall also turns the car. A yaw-rate term adds a torque about the vertical axis, which is what makes a car that hits a bank at an angle pivot instead of sliding flat along it. Below a very small horizontal speed nothing is applied. The function is reversed from the retail executable's wall friction code, and where the reconstructed C++ disagrees with the assembly the port follows the assembly.", + "constants": [ + { + "name": "Penetration gain", + "value": 3, + "unit": "force weight per metre of wall penetration; applied once per contact step at the 7 ms substep", + "source": "crates/rbr-physics/src/car_body/wall_friction.rs:24" + }, + { + "name": "Low-speed gain", + "value": 2, + "unit": "force weight per metre per second of horizontal speed in the low-speed term; applied once per contact step", + "source": "crates/rbr-physics/src/car_body/wall_friction.rs:27" + }, + { + "name": "High-speed gain", + "value": 0.01, + "unit": "force weight per metre per second of horizontal speed in the high-speed term; applied once per contact step", + "source": "crates/rbr-physics/src/car_body/wall_friction.rs:30" + }, + { + "name": "High-speed share", + "value": 0.5, + "unit": "dimensionless share of the high-speed term in the mixed weight; applied once per contact step", + "source": "crates/rbr-physics/src/car_body/wall_friction.rs:33" + }, + { + "name": "Low-speed share", + "value": 0.3, + "unit": "dimensionless share of the low-speed term in the mixed weight; applied once per contact step", + "source": "crates/rbr-physics/src/car_body/wall_friction.rs:36" + }, + { + "name": "Yaw gain", + "value": 2.5, + "unit": "seconds, the gain of the yaw rate in the torque about the vertical axis; applied once per contact step", + "source": "crates/rbr-physics/src/car_body/wall_friction.rs:39" + }, + { + "name": "Lever arm below the centre", + "value": -0.3, + "unit": "metres below the car's centre where the wall force is applied; applied once per contact step", + "source": "crates/rbr-physics/src/car_body/wall_friction.rs:42" + } + ], + "sources": [ + { + "path": "crates/rbr-physics/src/car_body/wall_friction.rs", + "line": 60 + }, + { + "path": "crates/rbr-physics/src/car_body/wall_friction.rs", + "line": 24 + } + ], + "capabilities": [ + "brake walls", + "snow banks", + "scrape friction", + "yaw torque" + ], + "tags": [ + "physics", + "collision", + "handling" + ] + }, + { + "id": "service-park-repair", + "node": "service-damage", + "name": "Service park repair", + "summary": "Between stages the crew repairs damaged parts against a time budget; running over the budget adds a penalty for every started minute.", + "behavior": "Each service event carries a budget: twenty minutes before a rally's first stage and after its first and third, forty before a night leg, and none otherwise. A rally's start gives the player a short service before the first stage. Damage is not a single number but a level per part, from sound to lost, and each level is scored for the manager's advice, with a lost part worth the most and a light scrape worth almost nothing. Repair orders run on a tree of parts: ordering a parent covers its children, taking an order off falls back to the damaged parts underneath it, and every repair draws its own duration between the file's unmount and mount times with a variance, then scales that by the crew's two factors. While the clock runs, the time left falls at the normal rate; pressing the repair key raises that rate so the remaining time is spent faster and the actual repairs advance with it. When the clock reaches zero the park closes and ten seconds are added for every minute that was started past the budget, whether or not the repairs finished. All of it is reversed from the retail executable's service code and its result page, and the community physics build patches only the crew's index and its own wheel and tyre damage objects.", + "constants": [ + { + "name": "Short service budget", + "value": 20, + "unit": "minutes of service time, converted to 1200 seconds at the park's open", + "source": "crates/rbr-gameplay/src/service/mod.rs:82" + }, + { + "name": "Long service budget", + "value": 40, + "unit": "minutes of service time before a night leg, converted to 2400 seconds at the park's open", + "source": "crates/rbr-gameplay/src/service/mod.rs:83" + }, + { + "name": "Late penalty", + "value": 10, + "unit": "seconds added per started minute over the budget; charged once when the park closes", + "source": "crates/rbr-gameplay/src/service/mod.rs:133" + }, + { + "name": "Repair clock rate", + "value": 30, + "unit": "seconds of repair per second of wall clock while the repair key is held; applied each service frame", + "source": "crates/rbr-gameplay/src/service/mod.rs:227" + } + ], + "sources": [ + { + "path": "crates/rbr-gameplay/src/service/mod.rs", + "line": 129 + }, + { + "path": "crates/rbr-gameplay/src/service/mod.rs", + "line": 473 + }, + { + "path": "crates/rbr-gameplay/src/car_damage.rs", + "line": 410 + } + ], + "capabilities": [ + "service park", + "repair orders", + "time budget", + "late penalty" + ], + "tags": [ + "gameplay", + "damage", + "rally" + ] + }, + { + "id": "weather-and-surface-choice", + "node": "weather", + "name": "Weather and surface choice", + "summary": "A stage's weather row decides cloud, rain, wetness and tyre wear, and wetness in turn decides which wet collision meshes and which tyre the car gets.", + "behavior": "Each stage map has a short list of rows in the season table, and a row fixes the track letter, the cloud, the rain preset, the wetness and the wear. A cloud letter sets the start time of day: the clear letter starts at a quarter past eight, the middle letter at twenty-two past midday and the overcast letter at twenty-three past five in the afternoon, and the clock then runs forward through the stage. Rain and snow presets carry a precipitation target, half scale for light and full scale for heavy, which drives the particle systems and is what a saved ghost records. Wetness is a small integer: at its top value the stage loads its wet collision meshes as well as its dry ones, so puddles and streams become surfaces with their own friction. The same wetness, with the car's chosen tyre wear, picks the entry in the stage's material file that the physics reads, which is how the same corner changes character between a dry morning and a wet afternoon. The weather rows and their byte layout are reversed from the retail executable's season table and its weather daemon; the renderer's clouds and rain are written fresh and only read the values the daemon leaves.", + "constants": [ + { + "name": "Clear-sky start hour", + "value": 8, + "unit": "hours after midnight for the clear cloud letter's start clock; set once at stage start", + "source": "crates/rbr-gameplay/src/weather.rs:404" + }, + { + "name": "Clear-sky start minute", + "value": 15, + "unit": "minutes past the start hour for the clear cloud letter; set once at stage start", + "source": "crates/rbr-gameplay/src/weather.rs:404" + }, + { + "name": "Light precipitation target", + "value": 50, + "unit": "percent of full precipitation for the light rain and light snow presets; read once per stage and stored in the ghost", + "source": "crates/rbr-gameplay/src/weather.rs:360" + }, + { + "name": "Heavy precipitation target", + "value": 100, + "unit": "percent of full precipitation for the heavy rain and heavy snow presets; read once per stage and stored in the ghost", + "source": "crates/rbr-gameplay/src/weather.rs:361" + } + ], + "sources": [ + { + "path": "crates/rbr-gameplay/src/weather.rs", + "line": 402 + }, + { + "path": "crates/rbr-gameplay/src/weather.rs", + "line": 340 + }, + { + "path": "crates/rbr-gameplay/src/weather.rs", + "line": 304 + } + ], + "capabilities": [ + "weather presets", + "time of day", + "wet surfaces", + "tyre choice" + ], + "tags": [ + "gameplay", + "weather", + "surfaces" + ] + }, + { + "id": "pace-note-callouts", + "node": "pace-notes", + "name": "Pace note callouts", + "summary": "As the car runs down the stage's distance the co-driver calls the next notes, each note fired once at a lookahead that grows with speed.", + "behavior": "The stage's notes are keys on the driveline, each with a tag for the kind of corner or hazard and flags for its modifiers. The ticker compares the car's distance with the keys and handles exactly one crossed key per step, which is what keeps start, split and finish events in order when the car covers a lot of ground in one frame. Notes ahead of the car are fired when they fall inside a lookahead that is the distance to the note scaled by the player's callout setting plus the car's own position, and only if the current speed is below a threshold that is also scaled by the distance, so a note is called earlier the faster the car is travelling and a slow car gets it late. A note fires once; the key carries a fired bit afterwards. Notes with a spoken word are separated from notes that only show a symbol, and the HUD keeps a short stack of the next symbol keys so the driver can read what is coming. The ticker, the lookahead arithmetic and the per-tag routing are reversed from the retail executable's animation-set ticker, with the tag tables taken from the stage's own note file rather than invented.", + "constants": [ + { + "name": "Pace speed multiplier", + "value": 3.6, + "unit": "metres per second of car speed per metre of distance to the note; compared once per note candidate per 7 ms substep", + "source": "crates/rbr-gameplay/src/markers.rs:16" + }, + { + "name": "Default callout distance", + "value": 4, + "unit": "metres of lookahead per metre of note distance, the game's starting setting; read once per note candidate per 7 ms substep", + "source": "crates/rbr-gameplay/src/markers.rs:30" + }, + { + "name": "Default speed threshold", + "value": 25, + "unit": "metres per second, the game's starting callout speed threshold; compared once per note candidate per 7 ms substep", + "source": "crates/rbr-gameplay/src/markers.rs:30" + }, + { + "name": "Pace stack depth", + "value": 5, + "unit": "symbol keys kept for the HUD's pace stack; refreshed once per HUD frame", + "source": "crates/rbr-gameplay/src/markers.rs:368" + } + ], + "sources": [ + { + "path": "crates/rbr-gameplay/src/markers.rs", + "line": 288 + }, + { + "path": "crates/rbr-gameplay/src/markers.rs", + "line": 373 + } + ], + "capabilities": [ + "pace notes", + "callout lookahead", + "marker ticker", + "HUD stack" + ], + "tags": [ + "gameplay", + "rally", + "audio" + ] + }, + { + "id": "stage-time-card", + "node": "stage-timing", + "name": "Stage time card", + "summary": "The time card finds the start either by crossing the start line or by moving, marks splits and the finish, and samples the car to catch a car that has stopped.", + "behavior": "The card starts before the start line with a fixed mid-day origin so that every reading is a time of day rather than a count from zero. The start is found two ways: the car's driveline distance reaching the start key, or, while the elapsed time is still on the unstarted side, the car moving faster than half a metre per second measured over one physics step. Each split and the finish is marked when the car's distance passes the key, and each carries the elapsed time at that moment. Every hundredth step while timing, the card samples the car's distance, and a sample in which the car moved less than four metres counts toward a stuck tally, which the flow uses to catch a car that has come to rest. The stage clock itself is a double seeded at its own starting value and advanced once per substep, with the addition rounded to single precision in the retail build and kept double under the community physics build; the countdown before the start is a separate seven-second clock, and crossing the line early while it still counts is a jump start penalised in seconds. The card, the start conditions and the clock seeding are reversed from the retail executable's timekeeper; the stuck tally feeds a flow that is reversed from the same executable.", + "constants": [ + { + "name": "Unstarted time origin", + "value": 43200, + "unit": "seconds after midnight held before timing starts; the origin of every split time", + "source": "crates/rbr-gameplay/src/timing.rs:16" + }, + { + "name": "Speed start", + "value": 0.5, + "unit": "metres per second the car must exceed over one 7 ms substep to start timing before the line", + "source": "crates/rbr-gameplay/src/timing.rs:20" + }, + { + "name": "Still distance", + "value": 4, + "unit": "metres of movement below which a sample counts the car as stopped; tested every 100 substeps", + "source": "crates/rbr-gameplay/src/timing.rs:22" + }, + { + "name": "Sample interval", + "value": 100, + "unit": "physics substeps between two distance samples of the time card, so roughly 0.7 seconds at 7 ms per substep", + "source": "crates/rbr-gameplay/src/timing.rs:24" + }, + { + "name": "Start countdown", + "value": 7, + "unit": "seconds of countdown before a stage start; decremented once per 7 ms substep", + "source": "crates/rbr-gameplay/src/race_clock.rs:25" + } + ], + "sources": [ + { + "path": "crates/rbr-gameplay/src/timing.rs", + "line": 269 + }, + { + "path": "crates/rbr-gameplay/src/timing.rs", + "line": 277 + }, + { + "path": "crates/rbr-gameplay/src/race_clock.rs", + "line": 216 + } + ], + "capabilities": [ + "stage timing", + "splits", + "speed start", + "stuck detection", + "countdown" + ], + "tags": [ + "gameplay", + "rally", + "timing" + ] + }, + { + "id": "gearbox-launch-and-shifts", + "node": "driveline", + "name": "Gearbox launch and shifts", + "summary": "The gearbox decides shifts by itself from pedal positions, engine speed and road speed, with a lockout after each request and a slip gate on the way down.", + "behavior": "In the low gears the box watches the pedals: with both pedals in the middle of their travel it treats the car as launching, and the exact fractions of pedal that open and close that window are fixed. A launch that lingers on the edge of the window for longer than a short timer forces the shift instead of waiting, and a neutral that lingers at zero speed does the same after a different timer. In the upper gears the shift request comes from the engine speed against a threshold, and every request starts a cooldown that blocks the next one, which is what makes the automatic gearbox feel deliberate rather than snatching. Before a downshift the box can ask a slip question: in the gears above second it compares the gear's rpm with the rev limit, and in second it asks whether the car is travelling faster than ten kilometres per hour, and a yes cancels the downshift. The differential locks handed to the drive are the largest of a throttle map, a brake map and a speed map, with the centre lock released while the handbrake is held, and a left-foot braking pair of maps is used when both pedals pass a configured threshold. The gear logic is reversed from the retail executable's vehicle control unit, with its supervisor's operation order taken from the assembly; the threshold values belong to the community physics build's car definitions.", + "constants": [ + { + "name": "Neutral speed limit", + "value": 0.5, + "unit": "metres per second, the speed boundary the low-gear launch logic uses; tested once per gear logic step at the 7 ms substep", + "source": "crates/rbr-physics/src/vcu/gear.rs:11" + }, + { + "name": "Low pedal threshold", + "value": 0.3, + "unit": "fraction of pedal travel marking the lower edge of the launch window; tested once per gear logic step", + "source": "crates/rbr-physics/src/vcu/gear.rs:12" + }, + { + "name": "High pedal threshold", + "value": 0.9, + "unit": "fraction of pedal travel marking the upper edge of the launch window; tested once per gear logic step", + "source": "crates/rbr-physics/src/vcu/gear.rs:13" + }, + { + "name": "Gear timer limit", + "value": 0.4, + "unit": "seconds a launch or neutral state may sit at its edge before a shift is forced; counted per gear logic step", + "source": "crates/rbr-physics/src/vcu/gear.rs:14" + }, + { + "name": "Downshift slip speed", + "value": "2.777_777_9", + "unit": "metres per second (ten kilometres per hour) above which a second-gear downshift is refused; tested once per shift request", + "source": "crates/rbr-physics/src/vcu/slip.rs:11" + }, + { + "name": "Clutch engagement scale", + "value": 900, + "unit": "clutch force units per unit of engagement; applied once per drive step at the 7 ms substep", + "source": "crates/rbr-physics/src/engine_driveline/drive_step.rs:10" + } + ], + "sources": [ + { + "path": "crates/rbr-physics/src/vcu/gear.rs", + "line": 35 + }, + { + "path": "crates/rbr-physics/src/vcu/slip.rs", + "line": 15 + }, + { + "path": "crates/rbr-physics/src/vcu/locks.rs", + "line": 31 + } + ], + "capabilities": [ + "automatic gearbox", + "shift lockout", + "downshift protection", + "differential locks", + "launch control" + ], + "tags": [ + "physics", + "driveline", + "handling" + ] + }, + { + "id": "control-payload-quantisation", + "node": "controls", + "name": "Control payload quantisation", + "summary": "Filtered pedal and steering values are quantised into small integers before the physics sees them, and the steering force becomes a force feedback magnitude.", + "behavior": "The driver's inputs are filtered first and then packed into a fixed-size payload of eight words that the physics reads every substep. Pedals take one of two hundred and fifty-six steps with half-step rounding, while steering is mapped onto a signed byte by spanning the range of a hundred and twenty-seven and a half steps and biasing by a hundred and twenty-eight, so the centre of the wheel lands on a byte value with slightly more resolution to the left than to the right. That quantisation is why a keyboard or a coarse wheel can feel notchy: the physics never sees a fractional value between two steps. Above the plain payload, the community physics build adds direct gear actions that are pressed rather than held, with a press threshold and a separate ignition threshold near full travel, so a button tap and a held button behave differently. The force feedback side takes the steering force the physics leaves, scales it by the player's strength setting, applies a dead zone and inverts it on request, and reports it as an integer magnitude with a fixed top of the range. The payload, its packing and the filter chain are reversed from the retail executable's input path; the community build's direct gears are reversed from that build, and the force feedback shaping is reversed from the exe's own force feedback call.", + "constants": [ + { + "name": "Steering span", + "value": 127.5, + "unit": "steps of steering travel across the full lock range; applied once per control payload at each 7 ms substep", + "source": "crates/rbr-gameplay/src/control.rs:27" + }, + { + "name": "Steering bias", + "value": 128, + "unit": "signed byte offset applied after the steering span; applied once per control payload at each 7 ms substep", + "source": "crates/rbr-gameplay/src/control.rs:28" + }, + { + "name": "Pedal span", + "value": 255, + "unit": "steps of pedal travel; applied once per control payload at each 7 ms substep", + "source": "crates/rbr-gameplay/src/control.rs:30" + }, + { + "name": "Direct gear press threshold", + "value": 0.999, + "unit": "fraction of an action's travel that counts as a press for the community build's direct gears; tested once per input poll", + "source": "crates/rbr-gameplay/src/control.rs:185" + }, + { + "name": "Ignition press threshold", + "value": 0.99, + "unit": "fraction of an action's travel that counts as ignition on; tested once per input poll", + "source": "crates/rbr-gameplay/src/control.rs:188" + }, + { + "name": "Force feedback magnitude ceiling", + "value": "10_000", + "unit": "force feedback units, the top of the magnitude range; computed once per force feedback update per 7 ms substep", + "source": "crates/rbr-gameplay/src/ffb.rs:36" + } + ], + "sources": [ + { + "path": "crates/rbr-gameplay/src/control.rs", + "line": 133 + }, + { + "path": "crates/rbr-gameplay/src/control.rs", + "line": 200 + }, + { + "path": "crates/rbr-gameplay/src/ffb.rs", + "line": 41 + } + ], + "capabilities": [ + "input quantisation", + "control payload", + "force feedback", + "direct gear actions" + ], + "tags": [ + "gameplay", + "input", + "physics" + ] + }, + { + "id": "chase-camera", + "node": "camera", + "name": "Chase camera and start orbit", + "summary": "The chase eye follows the car at a set radius and angle, lags a little behind, and is pushed out of stage geometry before it is drawn.", + "behavior": "The game's chase camera keeps its eye on a point a little ahead of and above the car body, and places the eye behind the car on a radius and a pitch angle that the player can change. The eye's wanted position is sampled into a short history and the drawn eye follows that history with a fixed lag, so the view swings smoothly through a hairpin instead of snapping to the car's new heading; a slight downhill lift keeps the car in frame over a crest. Before the lag is applied, the wanted eye is pulled in front of anything the stage geometry crosses between the eye and the look point, and pushed off nearby faces, with both corrections decaying over a few frames once contact is lost and with a faster catch-up while a push is still large. After the distance is clamped, the eye is put once more in front of whatever the look point and its own last place intersect, so a post or a rock cannot end up between the camera and the car. The bonnet view probes the ground and lifts itself by a bounded amount. The chase placement, the lag, the height window and the probes are reversed from the retail executable's camera code; the look-back key, the orbit clearance and the player's own chase settings are written for the Rust build and are not in the retail game. The field of view and the near plane belong to the fresh renderer.", + "constants": [ + { + "name": "Chase radius", + "value": 4.4, + "unit": "metres from the look point to the eye at the game's default setting; used once per camera frame", + "source": "crates/rbr-render/src/game_camera.rs:50" + }, + { + "name": "Chase field of view", + "value": 75, + "unit": "degrees horizontal, the Rust build's own chase field of view; used once per drawn frame", + "source": "crates/rbr-render/src/game_camera.rs:129" + }, + { + "name": "Look point height", + "value": 0.75, + "unit": "metres above the car body where the camera looks, with the look point 1.54 metres ahead; used once per camera frame", + "source": "crates/rbr-render/src/game_camera.rs:137" + }, + { + "name": "Wanted-eye push decay", + "value": 0.7, + "unit": "fraction kept per camera frame of the push applied to the wanted eye, so a push fades over a few frames", + "source": "crates/rbr-render/src/game_camera.rs:143" + }, + { + "name": "Drawn-eye push decay", + "value": 0.8, + "unit": "fraction kept per camera frame of the push applied to the drawn eye after the distance clamp", + "source": "crates/rbr-render/src/game_camera.rs:144" + }, + { + "name": "Eye lag history", + "value": 64, + "unit": "camera frames of wanted-eye history the drawn eye follows, about 0.45 seconds at 7 ms per physics substep", + "source": "crates/rbr-render/src/game_camera.rs:156" + } + ], + "sources": [ + { + "path": "crates/rbr-render/src/game_camera.rs", + "line": 192 + }, + { + "path": "crates/rbr-render/src/game_camera.rs", + "line": 203 + }, + { + "path": "crates/rbr-render/src/coords.rs", + "line": 23 + } + ], + "capabilities": [ + "chase camera", + "camera lag", + "camera collision", + "bonnet view", + "start orbit" + ], + "tags": [ + "render", + "camera", + "presentation" + ] + } + ] +} diff --git a/data/games/torcs.json b/data/games/torcs.json new file mode 100644 index 0000000..8f4c110 --- /dev/null +++ b/data/games/torcs.json @@ -0,0 +1,2612 @@ +{ + "version": 1, + "id": "torcs", + "title": "TORCS", + "repo": "https://github.com/jeremybennett/torcs", + "sha": "ccf4fe6aa90ba629ec4782e87169c46bddc5f6ee", + "nodes": [ + { + "id": "sim-loop", + "name": "Simulation step", + "purpose": "Fixed 500 Hz physics step that sanitizes driver commands and runs every car subsystem in a fixed order, driven by the race engine with robots polled at 50 Hz", + "paths": [ + "src/modules/simu/simuv2/simu.cpp", + "src/interfaces/raceman.h" + ], + "functions": [ + { + "name": "ctrlCheck", + "path": "src/modules/simu/simuv2/simu.cpp", + "lines": [ + 47, + 115 + ] + }, + { + "name": "SimUpdate", + "path": "src/modules/simu/simuv2/simu.cpp", + "lines": [ + 323, + 457 + ] + } + ], + "dependsOn": [] + }, + { + "id": "car-body", + "name": "Chassis rigid body", + "purpose": "Six-degree-of-freedom chassis: sums wheel, aero and gravity forces in the body frame, adds rolling resistance and integrates velocity and pose", + "paths": [ + "src/modules/simu/simuv2/car.cpp" + ], + "functions": [ + { + "name": "SimCarConfig", + "path": "src/modules/simu/simuv2/car.cpp", + "lines": [ + 29, + 139 + ] + }, + { + "name": "SimCarUpdateForces", + "path": "src/modules/simu/simuv2/car.cpp", + "lines": [ + 143, + 242 + ] + }, + { + "name": "SimCarUpdateSpeed", + "path": "src/modules/simu/simuv2/car.cpp", + "lines": [ + 245, + 272 + ] + }, + { + "name": "SimCarUpdatePos", + "path": "src/modules/simu/simuv2/car.cpp", + "lines": [ + 298, + 329 + ] + }, + { + "name": "SimCarUpdate", + "path": "src/modules/simu/simuv2/car.cpp", + "lines": [ + 409, + 424 + ] + } + ], + "dependsOn": [ + "sim-loop" + ] + }, + { + "id": "tyres", + "name": "Tyre model", + "purpose": "Per-wheel slip, magic-formula force with load sensitivity, camber and surface friction, skill-level grip help, wheel spin integration and the optional thermal and wear model", + "paths": [ + "src/modules/simu/simuv2/wheel.cpp", + "src/modules/simu/simuv2/categories.cpp" + ], + "functions": [ + { + "name": "SimWheelConfig", + "path": "src/modules/simu/simuv2/wheel.cpp", + "lines": [ + 27, + 135 + ] + }, + { + "name": "SimWheelUpdateRide", + "path": "src/modules/simu/simuv2/wheel.cpp", + "lines": [ + 167, + 207 + ] + }, + { + "name": "SimWheelUpdateForce", + "path": "src/modules/simu/simuv2/wheel.cpp", + "lines": [ + 212, + 344 + ] + }, + { + "name": "SimWheelUpdateRotation", + "path": "src/modules/simu/simuv2/wheel.cpp", + "lines": [ + 348, + 363 + ] + }, + { + "name": "SimUpdateFreeWheels", + "path": "src/modules/simu/simuv2/wheel.cpp", + "lines": [ + 367, + 393 + ] + }, + { + "name": "SimWheelUpdateTire", + "path": "src/modules/simu/simuv2/wheel.cpp", + "lines": [ + 396, + 475 + ] + } + ], + "dependsOn": [ + "car-body" + ] + }, + { + "id": "suspension", + "name": "Suspension and axles", + "purpose": "Coil spring with preload, two-slope bump and rebound dampers, travel stops, bellcrank ratio, anti-roll bar and third spring per axle", + "paths": [ + "src/modules/simu/simuv2/susp.cpp", + "src/modules/simu/simuv2/axle.cpp" + ], + "functions": [ + { + "name": "damperForce", + "path": "src/modules/simu/simuv2/susp.cpp", + "lines": [ + 41, + 72 + ] + }, + { + "name": "springForce", + "path": "src/modules/simu/simuv2/susp.cpp", + "lines": [ + 80, + 93 + ] + }, + { + "name": "SimSuspCheckIn", + "path": "src/modules/simu/simuv2/susp.cpp", + "lines": [ + 98, + 114 + ] + }, + { + "name": "SimSuspUpdate", + "path": "src/modules/simu/simuv2/susp.cpp", + "lines": [ + 119, + 132 + ] + }, + { + "name": "SimSuspConfig", + "path": "src/modules/simu/simuv2/susp.cpp", + "lines": [ + 137, + 155 + ] + }, + { + "name": "SimAxleConfig", + "path": "src/modules/simu/simuv2/axle.cpp", + "lines": [ + 24, + 47 + ] + }, + { + "name": "SimAxleUpdate", + "path": "src/modules/simu/simuv2/axle.cpp", + "lines": [ + 67, + 93 + ] + } + ], + "dependsOn": [ + "tyres" + ] + }, + { + "id": "aero", + "name": "Aerodynamics", + "purpose": "Quadratic body drag with damage and slipstream terms, ride-height ground effect and wing forces driven by angle of attack", + "paths": [ + "src/modules/simu/simuv2/aero.cpp" + ], + "functions": [ + { + "name": "SimAeroConfig", + "path": "src/modules/simu/simuv2/aero.cpp", + "lines": [ + 24, + 35 + ] + }, + { + "name": "SimAeroUpdate", + "path": "src/modules/simu/simuv2/aero.cpp", + "lines": [ + 38, + 107 + ] + }, + { + "name": "SimWingConfig", + "path": "src/modules/simu/simuv2/aero.cpp", + "lines": [ + 111, + 129 + ] + }, + { + "name": "SimWingUpdate", + "path": "src/modules/simu/simuv2/aero.cpp", + "lines": [ + 148, + 164 + ] + } + ], + "dependsOn": [ + "car-body" + ] + }, + { + "id": "drivetrain", + "name": "Engine and drivetrain", + "purpose": "Torque curve with engine braking and rev limiter, clutch and sequential gearbox with shift time, and free, limited-slip, viscous and locked differentials", + "paths": [ + "src/modules/simu/simuv2/engine.cpp", + "src/modules/simu/simuv2/transmission.cpp", + "src/modules/simu/simuv2/differential.cpp" + ], + "functions": [ + { + "name": "SimEngineConfig", + "path": "src/modules/simu/simuv2/engine.cpp", + "lines": [ + 24, + 97 + ] + }, + { + "name": "SimEngineUpdateTq", + "path": "src/modules/simu/simuv2/engine.cpp", + "lines": [ + 101, + 134 + ] + }, + { + "name": "SimEngineUpdateRpm", + "path": "src/modules/simu/simuv2/engine.cpp", + "lines": [ + 151, + 198 + ] + }, + { + "name": "SimTransmissionConfig", + "path": "src/modules/simu/simuv2/transmission.cpp", + "lines": [ + 25, + 155 + ] + }, + { + "name": "SimGearboxUpdate", + "path": "src/modules/simu/simuv2/transmission.cpp", + "lines": [ + 215, + 297 + ] + }, + { + "name": "SimTransmissionUpdate", + "path": "src/modules/simu/simuv2/transmission.cpp", + "lines": [ + 300, + 342 + ] + }, + { + "name": "SimDifferentialConfig", + "path": "src/modules/simu/simuv2/differential.cpp", + "lines": [ + 22, + 50 + ] + }, + { + "name": "SimDifferentialUpdate", + "path": "src/modules/simu/simuv2/differential.cpp", + "lines": [ + 143, + 273 + ] + } + ], + "dependsOn": [ + "tyres" + ] + }, + { + "id": "steer-brake", + "name": "Steering and brakes", + "purpose": "Rate-limited steering with Ackermann geometry and a hydraulic brake system split front to rear into disc torque", + "paths": [ + "src/modules/simu/simuv2/steer.cpp", + "src/modules/simu/simuv2/brake.cpp" + ], + "functions": [ + { + "name": "SimSteerConfig", + "path": "src/modules/simu/simuv2/steer.cpp", + "lines": [ + 22, + 29 + ] + }, + { + "name": "SimSteerUpdate", + "path": "src/modules/simu/simuv2/steer.cpp", + "lines": [ + 42, + 68 + ] + }, + { + "name": "SimBrakeConfig", + "path": "src/modules/simu/simuv2/brake.cpp", + "lines": [ + 22, + 33 + ] + }, + { + "name": "SimBrakeUpdate", + "path": "src/modules/simu/simuv2/brake.cpp", + "lines": [ + 36, + 44 + ] + }, + { + "name": "SimBrakeSystemConfig", + "path": "src/modules/simu/simuv2/brake.cpp", + "lines": [ + 47, + 53 + ] + }, + { + "name": "SimBrakeSystemUpdate", + "path": "src/modules/simu/simuv2/brake.cpp", + "lines": [ + 70, + 78 + ] + } + ], + "dependsOn": [ + "tyres" + ] + }, + { + "id": "collisions", + "name": "Collisions and damage", + "purpose": "Ground contact of the body, track barrier response by car corners, and impulse-based car-to-car and wall contacts, each adding damage points", + "paths": [ + "src/modules/simu/simuv2/collide.cpp" + ], + "functions": [ + { + "name": "SimCarCollideZ", + "path": "src/modules/simu/simuv2/collide.cpp", + "lines": [ + 25, + 93 + ] + }, + { + "name": "SimCarCollideXYScene", + "path": "src/modules/simu/simuv2/collide.cpp", + "lines": [ + 101, + 192 + ] + }, + { + "name": "SimCarCollideResponse", + "path": "src/modules/simu/simuv2/collide.cpp", + "lines": [ + 195, + 371 + ] + }, + { + "name": "SimCarWallCollideResponse", + "path": "src/modules/simu/simuv2/collide.cpp", + "lines": [ + 376, + 491 + ] + } + ], + "dependsOn": [ + "car-body" + ] + }, + { + "id": "robot-driver", + "name": "Robot opponent driver", + "purpose": "Reference opponent robot: corner speed from track radius, aero-aware braking distance, lookahead steering, overtaking offset, collision filters, driver aids and stuck recovery", + "paths": [ + "src/drivers/bt/driver.cpp", + "src/drivers/bt/opponent.cpp" + ], + "functions": [ + { + "name": "drive", + "path": "src/drivers/bt/driver.cpp", + "lines": [ + 171, + 197 + ] + }, + { + "name": "computeRadius", + "path": "src/drivers/bt/driver.cpp", + "lines": [ + 225, + 254 + ] + }, + { + "name": "getAllowedSpeed", + "path": "src/drivers/bt/driver.cpp", + "lines": [ + 258, + 278 + ] + }, + { + "name": "getAccel", + "path": "src/drivers/bt/driver.cpp", + "lines": [ + 293, + 307 + ] + }, + { + "name": "getBrake", + "path": "src/drivers/bt/driver.cpp", + "lines": [ + 324, + 355 + ] + }, + { + "name": "getGear", + "path": "src/drivers/bt/driver.cpp", + "lines": [ + 359, + 378 + ] + }, + { + "name": "getClutch", + "path": "src/drivers/bt/driver.cpp", + "lines": [ + 395, + 426 + ] + }, + { + "name": "getTargetPoint", + "path": "src/drivers/bt/driver.cpp", + "lines": [ + 429, + 493 + ] + }, + { + "name": "getOffset", + "path": "src/drivers/bt/driver.cpp", + "lines": [ + 497, + 631 + ] + }, + { + "name": "isStuck", + "path": "src/drivers/bt/driver.cpp", + "lines": [ + 672, + 687 + ] + }, + { + "name": "filterBrakeSpeed", + "path": "src/drivers/bt/driver.cpp", + "lines": [ + 733, + 739 + ] + }, + { + "name": "filterSColl", + "path": "src/drivers/bt/driver.cpp", + "lines": [ + 818, + 895 + ] + }, + { + "name": "filterABS", + "path": "src/drivers/bt/driver.cpp", + "lines": [ + 899, + 912 + ] + }, + { + "name": "filterTCL", + "path": "src/drivers/bt/driver.cpp", + "lines": [ + 916, + 923 + ] + }, + { + "name": "filterTrk", + "path": "src/drivers/bt/driver.cpp", + "lines": [ + 967, + 1000 + ] + }, + { + "name": "brakedist", + "path": "src/drivers/bt/driver.cpp", + "lines": [ + 1004, + 1011 + ] + }, + { + "name": "update", + "path": "src/drivers/bt/opponent.cpp", + "lines": [ + 40, + 129 + ] + }, + { + "name": "updateOverlapTimer", + "path": "src/drivers/bt/opponent.cpp", + "lines": [ + 144, + 165 + ] + } + ], + "dependsOn": [ + "sim-loop" + ] + }, + { + "id": "race-manager", + "name": "Race manager", + "purpose": "Race clock and step scheduling, lap counting, standings sort, race rules and penalties, and pit stop service time", + "paths": [ + "src/libs/raceengineclient/raceengine.cpp", + "src/interfaces/raceman.h" + ], + "functions": [ + { + "name": "ReUpdtPitTime", + "path": "src/libs/raceengineclient/raceengine.cpp", + "lines": [ + 52, + 83 + ] + }, + { + "name": "ReManage", + "path": "src/libs/raceengineclient/raceengine.cpp", + "lines": [ + 127, + 361 + ] + }, + { + "name": "ReSortCars", + "path": "src/libs/raceengineclient/raceengine.cpp", + "lines": [ + 364, + 399 + ] + }, + { + "name": "ReRaceRules", + "path": "src/libs/raceengineclient/raceengine.cpp", + "lines": [ + 404, + 611 + ] + }, + { + "name": "ReOneStep", + "path": "src/libs/raceengineclient/raceengine.cpp", + "lines": [ + 615, + 667 + ] + }, + { + "name": "ReUpdate", + "path": "src/libs/raceengineclient/raceengine.cpp", + "lines": [ + 710, + 778 + ] + } + ], + "dependsOn": [ + "sim-loop" + ] + } + ], + "cards": [ + { + "id": "sim-timestep", + "node": "sim-loop", + "name": "Fixed physics step and robot rate", + "summary": "Physics advances in fixed 2 millisecond steps, robots decide every 20 milliseconds, and the frame loop catches up with a step cap", + "behavior": "The race engine runs the physics in fixed steps of 0.002 seconds, so the simulation runs at 500 Hz whatever the frame rate. Each render frame runs as many steps as the wall clock requires, up to 2000 steps, and then gives up the backlog. Inside a step, robots are polled only when at least 0.02 seconds of race time passed since the last poll, so opponents and players send commands at 50 Hz. Each car step runs in a fixed order: atmosphere, command checks, steering, gearbox, engine torque, wheel positions, brakes, aero, wings, ride, axles, tyre forces and wear, transmission, wheel spin and chassis. Car-to-car contact runs after all cars move, then lap and rule bookkeeping runs.", + "constants": [ + { + "name": "physicsStep", + "value": 0.002, + "unit": "seconds per simulation step", + "source": "src/interfaces/raceman.h:56" + }, + { + "name": "robotStep", + "value": 0.02, + "unit": "seconds per robot update", + "source": "src/interfaces/raceman.h:57" + }, + { + "name": "maxCatchUpSteps", + "value": 2000, + "unit": "simulation steps per render frame", + "source": "src/libs/raceengineclient/raceengine.cpp:716" + } + ], + "sources": [ + { + "path": "src/libs/raceengineclient/raceengine.cpp", + "line": 644 + }, + { + "path": "src/libs/raceengineclient/raceengine.cpp", + "line": 657 + }, + { + "path": "src/libs/raceengineclient/raceengine.cpp", + "line": 726 + }, + { + "path": "src/modules/simu/simuv2/simu.cpp", + "line": 330 + }, + { + "path": "src/modules/simu/simuv2/simu.cpp", + "line": 361 + } + ], + "capabilities": [ + "fixed timestep", + "subsystem order", + "catch-up cap" + ], + "tags": [ + "physics", + "timestep" + ] + }, + { + "id": "command-sanitizing", + "node": "sim-loop", + "name": "Driver command limits and retirement coasting", + "summary": "Pedals and steering are clamped to valid ranges, while broken, retired or finished cars get scripted inputs", + "behavior": "Every step rejects invalid numbers and clamps throttle, brake and clutch to the range 0 to 1 and steering to the range minus 1 to 1. Clutch engagement is one minus the clutch pedal. A broken or eliminated car drops to neutral, lifts off, holds 0.1 brake and steers 0.1 toward the nearer track edge to pull off the racing line. After the flag a car is held to 0.2 throttle and gets at least 0.05 brake above 30 meters per second, so it cruises a cool-down lap.", + "constants": [ + { + "name": "retiredBrake", + "value": 0.1, + "unit": "brake pedal fraction", + "source": "src/modules/simu/simuv2/simu.cpp:68" + }, + { + "name": "retiredSteer", + "value": 0.1, + "unit": "steering fraction", + "source": "src/modules/simu/simuv2/simu.cpp:71" + }, + { + "name": "cooldownThrottle", + "value": 0.2, + "unit": "throttle pedal fraction", + "source": "src/modules/simu/simuv2/simu.cpp:86" + }, + { + "name": "cooldownSpeed", + "value": 30, + "unit": "meters per second", + "source": "src/modules/simu/simuv2/simu.cpp:87" + }, + { + "name": "cooldownBrake", + "value": 0.05, + "unit": "brake pedal fraction", + "source": "src/modules/simu/simuv2/simu.cpp:88" + } + ], + "sources": [ + { + "path": "src/modules/simu/simuv2/simu.cpp", + "line": 59 + }, + { + "path": "src/modules/simu/simuv2/simu.cpp", + "line": 66 + }, + { + "path": "src/modules/simu/simuv2/simu.cpp", + "line": 93 + }, + { + "path": "src/modules/simu/simuv2/simu.cpp", + "line": 114 + } + ], + "capabilities": [ + "input clamp", + "retirement", + "cool-down lap" + ], + "tags": [ + "input", + "race" + ] + }, + { + "id": "chassis-integration", + "node": "car-body", + "name": "Chassis forces and integration", + "summary": "Box-inertia rigid body under rotated gravity with explicit Euler steps, a yaw-rate cap and a 60 degree tilt limit", + "behavior": "Inertia comes from the car as a solid box of its length, width and height, with an extra factor on the yaw axis from the car data. Mass is the car mass plus the fuel on board, 1500 kilograms and 80 liters by default. Gravity is rotated into the body frame and added to the four wheel forces, the body drag, the wing forces and the axle lift, with moments from wheel positions, roll centers and ride height. Rolling resistance acts against velocity and yaw but is clamped so it can stop the car without pushing it backward in one step. Velocity and pose are integrated with explicit Euler steps, yaw rate is capped at 9 radians per second, and roll and pitch are clamped to 1.04 radians.", + "constants": [ + { + "name": "defaultMass", + "value": 1500, + "unit": "kilograms", + "source": "src/modules/simu/simuv2/car.cpp:44" + }, + { + "name": "defaultTank", + "value": 80, + "unit": "liters", + "source": "src/modules/simu/simuv2/car.cpp:52" + }, + { + "name": "boxInertiaFactor", + "value": 12, + "unit": "divisor of mass times size squared", + "source": "src/modules/simu/simuv2/car.cpp:66" + }, + { + "name": "yawRateCap", + "value": 9, + "unit": "radians per second, checked per simulation step at 500 Hz", + "source": "src/modules/simu/simuv2/car.cpp:256" + }, + { + "name": "tiltLimit", + "value": 1.04, + "unit": "radians", + "source": "src/modules/simu/simuv2/car.cpp:26" + } + ], + "sources": [ + { + "path": "src/modules/simu/simuv2/car.cpp", + "line": 153 + }, + { + "path": "src/modules/simu/simuv2/car.cpp", + "line": 171 + }, + { + "path": "src/modules/simu/simuv2/car.cpp", + "line": 209 + }, + { + "path": "src/modules/simu/simuv2/car.cpp", + "line": 228 + }, + { + "path": "src/modules/simu/simuv2/car.cpp", + "line": 247 + }, + { + "path": "src/modules/simu/simuv2/car.cpp", + "line": 315 + } + ], + "capabilities": [ + "rigid body", + "rolling resistance", + "euler integration" + ], + "tags": [ + "physics", + "movement" + ] + }, + { + "id": "tyre-magic-formula", + "node": "tyres", + "name": "Combined-slip magic formula", + "summary": "Longitudinal and lateral slip are merged into one slip magnitude that drives a simplified Pacejka curve", + "behavior": "Each wheel measures its velocity in the steered wheel frame. Longitudinal slip is the gap between road speed and rim speed over road speed, and lateral slip is the sine of the slip angle. Both are zero while the wheel is in the air. The two combine into one slip magnitude, capped at 1.5, which feeds a magic-formula curve. The shape factor comes from the tyre peak ratio, the stiffness from the cornering coefficient over the shape, and the curvature directly from the car data, with defaults of 30, 0.8 and 0.7. The resulting force is split back along the two slip directions, so a wheel spinning hard loses cornering grip. Both force components are then blended half and half with the previous step.", + "constants": [ + { + "name": "slipCap", + "value": 1.5, + "unit": "combined slip", + "source": "src/modules/simu/simuv2/wheel.cpp:302" + }, + { + "name": "defaultStiffness", + "value": 30, + "unit": "dimensionless", + "source": "src/modules/simu/simuv2/wheel.cpp:47" + }, + { + "name": "defaultPeakRatio", + "value": 0.8, + "unit": "dimensionless", + "source": "src/modules/simu/simuv2/wheel.cpp:48" + }, + { + "name": "defaultCurvature", + "value": 0.7, + "unit": "dimensionless", + "source": "src/modules/simu/simuv2/wheel.cpp:49" + }, + { + "name": "forceRelaxation", + "value": 50, + "unit": "percent of new value kept per simulation step at 500 Hz", + "source": "src/modules/simu/simuv2/wheel.cpp:323" + } + ], + "sources": [ + { + "path": "src/modules/simu/simuv2/wheel.cpp", + "line": 264 + }, + { + "path": "src/modules/simu/simuv2/wheel.cpp", + "line": 275 + }, + { + "path": "src/modules/simu/simuv2/wheel.cpp", + "line": 306 + }, + { + "path": "src/modules/simu/simuv2/wheel.cpp", + "line": 319 + }, + { + "path": "src/modules/simu/simuv2/wheel.cpp", + "line": 88 + } + ], + "capabilities": [ + "magic formula", + "combined slip", + "friction circle" + ], + "tags": [ + "tyres", + "grip", + "handling" + ] + }, + { + "id": "tyre-load-sensitivity", + "node": "tyres", + "name": "Load sensitivity, camber and surface", + "summary": "Friction falls off with vertical load along an exponential, rises slightly with negative camber and scales with the track surface", + "behavior": "The friction coefficient is the tyre mu times a load factor that starts at 1.6 for a light wheel and decays toward 0.8 as load grows past the operating load, which defaults to 1.2 times the static wheel weight. The tyre force is that friction times the vertical force, times the surface friction of the current track segment, times a camber term of plus or minus 5 percent that peaks when the effective camber angle times 18 reaches a quarter turn. Steering adds camber through the caster angle. Vertical force is zero when the wheel pulls away from the road, so the ground can push the car up but cannot pull it down.", + "constants": [ + { + "name": "lightLoadFactor", + "value": 1.6, + "unit": "friction multiplier", + "source": "src/modules/simu/simuv2/wheel.cpp:50" + }, + { + "name": "heavyLoadFactor", + "value": 0.8, + "unit": "friction multiplier", + "source": "src/modules/simu/simuv2/wheel.cpp:51" + }, + { + "name": "operatingLoadShare", + "value": 1.2, + "unit": "multiplier on static wheel weight", + "source": "src/modules/simu/simuv2/wheel.cpp:52" + }, + { + "name": "camberGain", + "value": 0.05, + "unit": "friction fraction", + "source": "src/modules/simu/simuv2/wheel.cpp:311" + }, + { + "name": "camberFrequency", + "value": 18, + "unit": "per radian", + "source": "src/modules/simu/simuv2/wheel.cpp:311" + }, + { + "name": "defaultTyreMu", + "value": 1, + "unit": "friction coefficient", + "source": "src/modules/simu/simuv2/wheel.cpp:39" + } + ], + "sources": [ + { + "path": "src/modules/simu/simuv2/wheel.cpp", + "line": 309 + }, + { + "path": "src/modules/simu/simuv2/wheel.cpp", + "line": 311 + }, + { + "path": "src/modules/simu/simuv2/wheel.cpp", + "line": 292 + }, + { + "path": "src/modules/simu/simuv2/wheel.cpp", + "line": 246 + }, + { + "path": "src/modules/simu/simuv2/wheel.cpp", + "line": 92 + } + ], + "capabilities": [ + "load sensitivity", + "camber", + "surface friction" + ], + "tags": [ + "tyres", + "grip" + ] + }, + { + "id": "skill-grip-help", + "node": "tyres", + "name": "Skill-level grip help and skid feedback", + "summary": "Easier driver skill levels gain extra tyre force at high slip, and a skid value drives tyre sound", + "behavior": "The tyre force is multiplied by one plus slip times a factor picked by skill level: 0.4 for the easiest level, then 0.35 and 0.3, and none for the two hardest levels. A sliding car on an easy level therefore keeps more grip than the curve alone would give. Above 2 meters per second each wheel also reports a skid level of slip times vertical load times 0.0002, capped at 1, for sound and smoke, and zero below that speed.", + "constants": [ + { + "name": "rookieSkidFactor", + "value": 0.4, + "unit": "grip gain per unit slip", + "source": "src/modules/simu/simuv2/categories.cpp:32" + }, + { + "name": "amateurSkidFactor", + "value": 0.35, + "unit": "grip gain per unit slip", + "source": "src/modules/simu/simuv2/categories.cpp:32" + }, + { + "name": "semiProSkidFactor", + "value": 0.3, + "unit": "grip gain per unit slip", + "source": "src/modules/simu/simuv2/categories.cpp:32" + }, + { + "name": "skidSoundGain", + "value": 0.0002, + "unit": "per newton of load per unit slip", + "source": "src/modules/simu/simuv2/wheel.cpp:288" + }, + { + "name": "skidSoundMinSpeed", + "value": 2, + "unit": "meters per second", + "source": "src/modules/simu/simuv2/wheel.cpp:285" + } + ], + "sources": [ + { + "path": "src/modules/simu/simuv2/wheel.cpp", + "line": 306 + }, + { + "path": "src/modules/simu/simuv2/wheel.cpp", + "line": 285 + }, + { + "path": "src/modules/simu/simuv2/wheel.cpp", + "line": 288 + } + ], + "capabilities": [ + "difficulty levels", + "skid sound" + ], + "tags": [ + "tyres", + "difficulty", + "effects" + ] + }, + { + "id": "tyre-thermal-wear", + "node": "tyres", + "name": "Tyre heat, pressure, wear and graining", + "summary": "At the hardest skill level with tyre rules on, tyres heat from slip, cool by convection, wear and grain, and grip peaks near an ideal temperature", + "behavior": "Heat input is load times rim speed times a hysteresis term of 0.05 for rolling, scaled by remaining tread and pressure, plus 0.5 times slip. Heat loss is convection of 5.9 plus 3.7 per meter per second of speed, times the temperature gap and the tyre surface. The heat capacity combines rubber, whose specific heat changes with temperature, and the nitrogen fill, and pressure follows temperature at constant volume. Wear grows with pressure, slip, speed and load. Graining builds when the tyre runs below three quarters of the way from ambient to the ideal temperature, 95 degrees Celsius by default. Grip is between 75 and 100 percent depending on the distance from the ideal temperature, and graining removes up to a further tenth.", + "constants": [ + { + "name": "rollingHysteresis", + "value": 0.05, + "unit": "energy fraction", + "source": "src/modules/simu/simuv2/wheel.cpp:414" + }, + { + "name": "slipHysteresis", + "value": 0.5, + "unit": "energy fraction per unit slip", + "source": "src/modules/simu/simuv2/wheel.cpp:414" + }, + { + "name": "convectionBase", + "value": 5.9, + "unit": "watts per square meter per kelvin", + "source": "src/modules/simu/simuv2/wheel.cpp:422" + }, + { + "name": "convectionPerSpeed", + "value": 3.7, + "unit": "watts per square meter per kelvin per meter per second", + "source": "src/modules/simu/simuv2/wheel.cpp:422" + }, + { + "name": "idealTemperatureCelsius", + "value": 95, + "unit": "degrees Celsius", + "source": "src/modules/simu/simuv2/wheel.cpp:105" + }, + { + "name": "grainingMaxLoss", + "value": 10, + "unit": "divisor on graining", + "source": "src/modules/simu/simuv2/wheel.cpp:474" + } + ], + "sources": [ + { + "path": "src/modules/simu/simuv2/wheel.cpp", + "line": 397 + }, + { + "path": "src/modules/simu/simuv2/wheel.cpp", + "line": 417 + }, + { + "path": "src/modules/simu/simuv2/wheel.cpp", + "line": 450 + }, + { + "path": "src/modules/simu/simuv2/wheel.cpp", + "line": 454 + }, + { + "path": "src/modules/simu/simuv2/wheel.cpp", + "line": 460 + }, + { + "path": "src/modules/simu/simuv2/wheel.cpp", + "line": 474 + } + ], + "capabilities": [ + "tyre temperature", + "tyre wear", + "graining" + ], + "tags": [ + "tyres", + "strategy", + "simulation" + ] + }, + { + "id": "suspension-spring-damper", + "node": "suspension", + "name": "Spring, damper and wheel hop", + "summary": "Each corner has a preloaded push-only spring, two-slope dampers and a separately integrated wheel mass", + "behavior": "Spring force is stiffness times compression from rest plus preload, and it never pulls. Bump and rebound dampers each use a slow slope below a threshold velocity of 0.5 meters per second and a fast slope above it, with the damper speed capped at 10 meters per second. The total force passes through the bellcrank ratio and is zero when negative. Travel stops at the packers and at the maximum course, 0.5 meters by default, and the default spring rate is 175000 newtons per meter. The wheel has its own mass, 20 kilograms by default, integrated against the suspension force. The wheel counts as airborne when the road lies below its full extension.", + "constants": [ + { + "name": "defaultSpringRate", + "value": 175000, + "unit": "newtons per meter", + "source": "src/modules/simu/simuv2/susp.cpp:139" + }, + { + "name": "defaultTravel", + "value": 0.5, + "unit": "meters", + "source": "src/modules/simu/simuv2/susp.cpp:140" + }, + { + "name": "damperKneeSpeed", + "value": 0.5, + "unit": "meters per second", + "source": "src/modules/simu/simuv2/susp.cpp:147" + }, + { + "name": "damperSpeedCap", + "value": 10, + "unit": "meters per second", + "source": "src/modules/simu/simuv2/susp.cpp:50" + }, + { + "name": "defaultWheelMass", + "value": 20, + "unit": "kilograms", + "source": "src/modules/simu/simuv2/wheel.cpp:53" + } + ], + "sources": [ + { + "path": "src/modules/simu/simuv2/susp.cpp", + "line": 86 + }, + { + "path": "src/modules/simu/simuv2/susp.cpp", + "line": 63 + }, + { + "path": "src/modules/simu/simuv2/susp.cpp", + "line": 101 + }, + { + "path": "src/modules/simu/simuv2/susp.cpp", + "line": 121 + }, + { + "path": "src/modules/simu/simuv2/wheel.cpp", + "line": 186 + }, + { + "path": "src/modules/simu/simuv2/wheel.cpp", + "line": 236 + } + ], + "capabilities": [ + "suspension", + "damping", + "wheel hop" + ], + "tags": [ + "physics", + "suspension" + ] + }, + { + "id": "anti-roll-third-spring", + "node": "suspension", + "name": "Anti-roll bar and third spring", + "summary": "Each axle couples its two wheels through an anti-roll bar on the travel difference and a third spring on the mean travel", + "behavior": "Per axle, the anti-roll force is the bar stiffness times the difference between left and right suspension travel, added to one wheel and subtracted from the other. A third spring and damper act on the average travel of the two wheels, and half of its force goes to each wheel while it sits inside its travel. Axle inertia and roll center height both default to 0.15. The bar stiffness defaults to zero, so the bar only works when the car data sets it.", + "constants": [ + { + "name": "defaultAxleInertia", + "value": 0.15, + "unit": "kilogram square meters", + "source": "src/modules/simu/simuv2/axle.cpp:32" + }, + { + "name": "defaultRollCenter", + "value": 0.15, + "unit": "meters", + "source": "src/modules/simu/simuv2/axle.cpp:33" + } + ], + "sources": [ + { + "path": "src/modules/simu/simuv2/axle.cpp", + "line": 78 + }, + { + "path": "src/modules/simu/simuv2/axle.cpp", + "line": 81 + }, + { + "path": "src/modules/simu/simuv2/axle.cpp", + "line": 85 + }, + { + "path": "src/modules/simu/simuv2/axle.cpp", + "line": 90 + } + ], + "capabilities": [ + "anti-roll bar", + "third spring", + "roll center" + ], + "tags": [ + "suspension", + "handling" + ] + }, + { + "id": "aero-drag-ground-effect", + "node": "aero", + "name": "Body drag and ground effect", + "summary": "Drag grows with the square of speed and with damage, and body lift turns into downforce that fades as ride height rises", + "behavior": "The drag area is 0.645 times the drag coefficient times frontal area, with defaults of 0.4 and 2.5 square meters. Drag is that area times forward airspeed squared, raised by damage points over 10000, times the square of the slipstream factor. Front and rear body lift use their own coefficients times speed squared times a ground-effect factor. That factor is 2 times the exponential of minus 3 times the fourth power of 1.5 times the summed ride heights, so lowering the car raises downforce sharply. Lift also scales with how much the car travels forward and not sideways.", + "constants": [ + { + "name": "dragAreaFactor", + "value": 0.645, + "unit": "kilograms per cubic meter scale", + "source": "src/modules/simu/simuv2/aero.cpp:33" + }, + { + "name": "defaultDragCoefficient", + "value": 0.4, + "unit": "dimensionless", + "source": "src/modules/simu/simuv2/aero.cpp:29" + }, + { + "name": "defaultFrontalArea", + "value": 2.5, + "unit": "square meters", + "source": "src/modules/simu/simuv2/aero.cpp:30" + }, + { + "name": "damageDragDivisor", + "value": 10000, + "unit": "damage points", + "source": "src/modules/simu/simuv2/aero.cpp:99" + }, + { + "name": "groundEffectDecay", + "value": -3, + "unit": "exponent scale", + "source": "src/modules/simu/simuv2/aero.cpp:104" + }, + { + "name": "rideHeightScale", + "value": 1.5, + "unit": "multiplier on summed ride heights", + "source": "src/modules/simu/simuv2/aero.cpp:101" + } + ], + "sources": [ + { + "path": "src/modules/simu/simuv2/aero.cpp", + "line": 86 + }, + { + "path": "src/modules/simu/simuv2/aero.cpp", + "line": 99 + }, + { + "path": "src/modules/simu/simuv2/aero.cpp", + "line": 101 + }, + { + "path": "src/modules/simu/simuv2/aero.cpp", + "line": 105 + } + ], + "capabilities": [ + "aerodynamic drag", + "ground effect", + "damage drag" + ], + "tags": [ + "aero", + "speed", + "grip" + ] + }, + { + "id": "slipstream", + "node": "aero", + "name": "Slipstream and lead-car drag", + "summary": "Following closely behind a car cuts drag, and a car close behind also slightly cuts drag of the car ahead", + "behavior": "Above 10 meters per second each car checks every other car that also moves above 10 and whose heading is within 0.1396 radians of its own. When the other car is ahead within 10 degrees of straight ahead, the drag factor drops to one minus the exponential of minus 2 times the gap over that car drag area times its speed. When the other car is close behind within 8 degrees, the factor drops by at most 15 percent along a faster exponential. The smallest factor wins, and because drag uses its square the tow effect is strong at short range.", + "constants": [ + { + "name": "slipstreamMinSpeed", + "value": 10, + "unit": "meters per second", + "source": "src/modules/simu/simuv2/aero.cpp:53" + }, + { + "name": "headingTolerance", + "value": 0.1396, + "unit": "radians", + "source": "src/modules/simu/simuv2/aero.cpp:67" + }, + { + "name": "behindCone", + "value": 2.9671, + "unit": "radians from the rear direction", + "source": "src/modules/simu/simuv2/aero.cpp:68" + }, + { + "name": "trailingDecay", + "value": 2, + "unit": "exponent scale", + "source": "src/modules/simu/simuv2/aero.cpp:70" + }, + { + "name": "leadReduction", + "value": 0.15, + "unit": "drag fraction", + "source": "src/modules/simu/simuv2/aero.cpp:77" + }, + { + "name": "leadDecay", + "value": 8, + "unit": "exponent scale", + "source": "src/modules/simu/simuv2/aero.cpp:77" + } + ], + "sources": [ + { + "path": "src/modules/simu/simuv2/aero.cpp", + "line": 54 + }, + { + "path": "src/modules/simu/simuv2/aero.cpp", + "line": 67 + }, + { + "path": "src/modules/simu/simuv2/aero.cpp", + "line": 72 + }, + { + "path": "src/modules/simu/simuv2/aero.cpp", + "line": 99 + } + ], + "capabilities": [ + "drafting", + "tow" + ], + "tags": [ + "aero", + "racing", + "ai" + ] + }, + { + "id": "wings", + "node": "aero", + "name": "Front and rear wings", + "summary": "Wing force grows with speed squared and the sine of the angle of attack, with downforce four times the drag", + "behavior": "Each wing gets a drag constant of minus 1.23 times its area and a downforce constant four times larger. The angle of attack is the wing angle plus the pitch of the airflow from vertical and forward speed. While moving forward, the wing drag is the drag constant times airspeed squared times the sine of that angle, raised by damage, and the downforce uses the larger constant. Both are zero when the car rolls backward. The rear wing angle also adds to the body drag coefficient, and a pit setup change recomputes it.", + "constants": [ + { + "name": "wingDragFactor", + "value": -1.23, + "unit": "kilograms per cubic meter scale per square meter", + "source": "src/modules/simu/simuv2/aero.cpp:123" + }, + { + "name": "downforceToDrag", + "value": 4, + "unit": "multiplier", + "source": "src/modules/simu/simuv2/aero.cpp:124" + } + ], + "sources": [ + { + "path": "src/modules/simu/simuv2/aero.cpp", + "line": 153 + }, + { + "path": "src/modules/simu/simuv2/aero.cpp", + "line": 159 + }, + { + "path": "src/modules/simu/simuv2/aero.cpp", + "line": 160 + }, + { + "path": "src/modules/simu/simuv2/aero.cpp", + "line": 127 + } + ], + "capabilities": [ + "wings", + "downforce", + "setup" + ], + "tags": [ + "aero", + "grip", + "setup" + ] + }, + { + "id": "engine-torque", + "node": "drivetrain", + "name": "Engine torque, braking and rev limiter", + "summary": "Torque comes from a piecewise-linear curve, blends into engine braking off-throttle, and cuts at the rev limiter", + "behavior": "The torque curve is linear between data points, and peak torque and peak power are searched between idle and the limiter. Engine output is the curve torque times throttle times one plus the braking share, minus the braking share. The braking share is the braking coefficient, 0.33 by default, scaled from zero at idle to full at maximum revs. Above the limiter the engine is held at the limiter with zero torque. An empty tank or a broken car stops the engine. Defaults are a limiter of 800, a maximum of 1000 and an idle of 150 radians per second, with 0.2423 kilogram square meters of inertia.", + "constants": [ + { + "name": "defaultRevLimiter", + "value": 800, + "unit": "radians per second", + "source": "src/modules/simu/simuv2/engine.cpp:39" + }, + { + "name": "defaultMaxRevs", + "value": 1000, + "unit": "radians per second", + "source": "src/modules/simu/simuv2/engine.cpp:41" + }, + { + "name": "defaultIdle", + "value": 150, + "unit": "radians per second", + "source": "src/modules/simu/simuv2/engine.cpp:43" + }, + { + "name": "defaultEngineInertia", + "value": 0.2423, + "unit": "kilogram square meters", + "source": "src/modules/simu/simuv2/engine.cpp:44" + }, + { + "name": "defaultEngineBraking", + "value": 0.33, + "unit": "torque fraction", + "source": "src/modules/simu/simuv2/engine.cpp:46" + } + ], + "sources": [ + { + "path": "src/modules/simu/simuv2/engine.cpp", + "line": 84 + }, + { + "path": "src/modules/simu/simuv2/engine.cpp", + "line": 113 + }, + { + "path": "src/modules/simu/simuv2/engine.cpp", + "line": 120 + }, + { + "path": "src/modules/simu/simuv2/engine.cpp", + "line": 122 + }, + { + "path": "src/modules/simu/simuv2/engine.cpp", + "line": 126 + } + ], + "capabilities": [ + "torque curve", + "engine braking", + "rev limiter", + "fuel use" + ], + "tags": [ + "engine", + "acceleration" + ] + }, + { + "id": "clutch-gearbox", + "node": "drivetrain", + "name": "Clutch and gear shifts", + "summary": "A shift starts a clutch release timer, cuts throttle, and blends engine speed with wheel speed through a steep clutch curve", + "behavior": "Requesting a higher or lower gear inside the gearbox range changes gear at once and starts the clutch release time, 0.2 seconds by default. While that timer runs, a clutch that reads above 0.99 engagement is forced open and throttle is capped at 0.1, which gives the shift its torque gap. Drive torque reaching the differential is engine torque times the overall ratio times three times clutch engagement, capped at 1. Engine speed is blended between wheel-driven speed and free-revving speed by the fourth power of engagement, and held between idle and maximum revs. Driveline inertia is also blended between the coupled and free values.", + "constants": [ + { + "name": "defaultShiftTime", + "value": 0.2, + "unit": "seconds", + "source": "src/modules/simu/simuv2/transmission.cpp:41" + }, + { + "name": "clutchForceOpenAbove", + "value": 0.99, + "unit": "clutch engagement fraction", + "source": "src/modules/simu/simuv2/transmission.cpp:242" + }, + { + "name": "shiftThrottleCap", + "value": 0.1, + "unit": "throttle fraction", + "source": "src/modules/simu/simuv2/transmission.cpp:245" + }, + { + "name": "clutchTorqueGain", + "value": 3, + "unit": "multiplier on engagement", + "source": "src/modules/simu/simuv2/transmission.cpp:305" + }, + { + "name": "clutchSlipFloor", + "value": 0.01, + "unit": "clutch engagement fraction", + "source": "src/modules/simu/simuv2/engine.cpp:183" + } + ], + "sources": [ + { + "path": "src/modules/simu/simuv2/transmission.cpp", + "line": 236 + }, + { + "path": "src/modules/simu/simuv2/transmission.cpp", + "line": 250 + }, + { + "path": "src/modules/simu/simuv2/transmission.cpp", + "line": 273 + }, + { + "path": "src/modules/simu/simuv2/transmission.cpp", + "line": 310 + }, + { + "path": "src/modules/simu/simuv2/engine.cpp", + "line": 184 + }, + { + "path": "src/modules/simu/simuv2/engine.cpp", + "line": 186 + } + ], + "capabilities": [ + "clutch", + "gear shift", + "driveline inertia" + ], + "tags": [ + "gears", + "engine", + "transmission" + ] + }, + { + "id": "differentials", + "node": "drivetrain", + "name": "Differential types", + "summary": "Rear, front or four-wheel drive with free, limited-slip, viscous or locked differentials", + "behavior": "A free differential splits drive torque in half and adds the spider torque from the difference between the two wheel reactions. A limited-slip unit allows a speed ratio between the wheels that shrinks as drive torque grows. Beyond that ratio it pulls the wheel speeds together and biases torque. Above the locking torque, 3000 newton meters by default or 0.33 of that under braking, it acts as a locked spool. A viscous coupler sends the minimum bias, 0.05 by default, until the wheels differ, then adds up to the maximum bias, 0.8 by default, along an exponential of the speed difference. Four-wheel drive feeds a center differential that drives the front and rear units. Wheel brake torque is applied after drive and may stop a wheel but never reverses it.", + "constants": [ + { + "name": "defaultLockingTorque", + "value": 3000, + "unit": "newton meters", + "source": "src/modules/simu/simuv2/differential.cpp:31" + }, + { + "name": "brakeLockingShare", + "value": 0.33, + "unit": "fraction of locking torque", + "source": "src/modules/simu/simuv2/differential.cpp:32" + }, + { + "name": "defaultMinBias", + "value": 0.05, + "unit": "torque fraction", + "source": "src/modules/simu/simuv2/differential.cpp:27" + }, + { + "name": "defaultMaxBias", + "value": 0.8, + "unit": "torque fraction", + "source": "src/modules/simu/simuv2/differential.cpp:28" + }, + { + "name": "defaultSlipBias", + "value": 0.03, + "unit": "wheel speed ratio", + "source": "src/modules/simu/simuv2/differential.cpp:30" + }, + { + "name": "freeSplit", + "value": 0.5, + "unit": "torque fraction", + "source": "src/modules/simu/simuv2/differential.cpp:176" + } + ], + "sources": [ + { + "path": "src/modules/simu/simuv2/differential.cpp", + "line": 173 + }, + { + "path": "src/modules/simu/simuv2/differential.cpp", + "line": 181 + }, + { + "path": "src/modules/simu/simuv2/differential.cpp", + "line": 194 + }, + { + "path": "src/modules/simu/simuv2/differential.cpp", + "line": 220 + }, + { + "path": "src/modules/simu/simuv2/differential.cpp", + "line": 242 + }, + { + "path": "src/modules/simu/simuv2/transmission.cpp", + "line": 325 + } + ], + "capabilities": [ + "limited slip", + "viscous coupler", + "four-wheel drive" + ], + "tags": [ + "drivetrain", + "traction", + "handling" + ] + }, + { + "id": "steering", + "node": "steer-brake", + "name": "Rate-limited Ackermann steering", + "summary": "The steering command maps to a lock angle, moves at a capped rate, and the inner wheel turns more than the outer", + "behavior": "The command of minus 1 to 1 is multiplied by the steering lock, 0.43 radians by default. The road-wheel angle can change by at most the steering speed, 1 radian per second by default, so a robot or a keyboard cannot flick the wheels instantly. The outer wheel takes the steered angle and the inner wheel gets a sharper angle from the wheelbase and track width, which gives Ackermann geometry and a clean low-speed turn.", + "constants": [ + { + "name": "defaultSteerLock", + "value": 0.43, + "unit": "radians", + "source": "src/modules/simu/simuv2/steer.cpp:26" + }, + { + "name": "defaultSteerRate", + "value": 1, + "unit": "radians per second", + "source": "src/modules/simu/simuv2/steer.cpp:27" + } + ], + "sources": [ + { + "path": "src/modules/simu/simuv2/steer.cpp", + "line": 50 + }, + { + "path": "src/modules/simu/simuv2/steer.cpp", + "line": 53 + }, + { + "path": "src/modules/simu/simuv2/steer.cpp", + "line": 59 + }, + { + "path": "src/modules/simu/simuv2/steer.cpp", + "line": 61 + } + ], + "capabilities": [ + "steering lock", + "steering rate", + "ackermann" + ], + "tags": [ + "steering", + "input" + ] + }, + { + "id": "brakes", + "node": "steer-brake", + "name": "Brake pressure, balance and heat", + "summary": "Brake pedal sets line pressure split by the brake balance, and each disc turns pressure into torque and heat", + "behavior": "Brake pedal times the system pressure, 1000000 pascal by default, gives line pressure. The front axle gets the repartition share, half by default, and the rear the rest. Each disc torque is the diameter times 0.5 times pad area times pad friction times pressure, with defaults of 0.2 meters, 0.002 square meters and 0.3. Brake temperature falls each step by 0.0001 per meter per second of speed plus 0.0002, and rises with pressure, disc radius and wheel speed, held between 0 and 1. On undriven wheels the brake torque may stop the wheel but never spins it backward.", + "constants": [ + { + "name": "defaultLinePressure", + "value": 1000000, + "unit": "pascal at full pedal", + "source": "src/modules/simu/simuv2/brake.cpp:52" + }, + { + "name": "defaultFrontShare", + "value": 0.5, + "unit": "fraction", + "source": "src/modules/simu/simuv2/brake.cpp:51" + }, + { + "name": "defaultDiscDiameter", + "value": 0.2, + "unit": "meters", + "source": "src/modules/simu/simuv2/brake.cpp:26" + }, + { + "name": "defaultPadArea", + "value": 0.002, + "unit": "square meters", + "source": "src/modules/simu/simuv2/brake.cpp:27" + }, + { + "name": "defaultPadFriction", + "value": 0.3, + "unit": "friction coefficient", + "source": "src/modules/simu/simuv2/brake.cpp:28" + }, + { + "name": "brakeCoolingBase", + "value": 0.0002, + "unit": "temperature units per simulation step at 500 Hz", + "source": "src/modules/simu/simuv2/brake.cpp:40" + } + ], + "sources": [ + { + "path": "src/modules/simu/simuv2/brake.cpp", + "line": 29 + }, + { + "path": "src/modules/simu/simuv2/brake.cpp", + "line": 38 + }, + { + "path": "src/modules/simu/simuv2/brake.cpp", + "line": 42 + }, + { + "path": "src/modules/simu/simuv2/brake.cpp", + "line": 76 + }, + { + "path": "src/modules/simu/simuv2/wheel.cpp", + "line": 386 + } + ], + "capabilities": [ + "brake balance", + "disc torque", + "brake temperature" + ], + "tags": [ + "braking", + "setup" + ] + }, + { + "id": "ground-contact", + "node": "collisions", + "name": "Body ground contact", + "summary": "Car corners and body that sink below the track surface correct the tilt and lose their downward velocity, with damage on hard landings", + "behavior": "Each step the four body corners are compared with the track height below them. A corner under the surface tilts the car pitch and roll back toward the surface. When the body center sinks under the road, the velocity component into the surface normal is removed, which makes a fully inelastic landing. A landing faster than 5 meters per second into the surface raises a crash flag. Unless the car has finished, it adds damage from the surface damage factor times the impact speed, scaled by the rules and the skill level.", + "constants": [ + { + "name": "crashImpactSpeed", + "value": -5, + "unit": "meters per second along the surface normal", + "source": "src/modules/simu/simuv2/collide.cpp:28" + } + ], + "sources": [ + { + "path": "src/modules/simu/simuv2/collide.cpp", + "line": 53 + }, + { + "path": "src/modules/simu/simuv2/collide.cpp", + "line": 61 + }, + { + "path": "src/modules/simu/simuv2/collide.cpp", + "line": 77 + }, + { + "path": "src/modules/simu/simuv2/collide.cpp", + "line": 84 + }, + { + "path": "src/modules/simu/simuv2/collide.cpp", + "line": 89 + } + ], + "capabilities": [ + "ground collision", + "landing", + "damage" + ], + "tags": [ + "collision", + "jump", + "damage" + ] + }, + { + "id": "barrier-contact", + "node": "collisions", + "name": "Track barrier contact", + "summary": "A car corner past the track edge is pushed back, loses normal speed through barrier friction and rebound, and gets a yaw kick", + "behavior": "For each body corner outside the left or right edge, the car is moved back inside along the barrier normal. Velocity into the barrier is reduced by the barrier friction, and the corner lever arm turns that into a yaw change divided by 10 and capped at 6 radians per second. Damage is the barrier damage factor times half the squared normal impact speed, plus 0.005 times the glancing term times speed. The normal velocity then bounces by the barrier rebound factor. Wall hits can also void the lap time under the race rules.", + "constants": [ + { + "name": "yawKickScale", + "value": 10, + "unit": "divisor", + "source": "src/modules/simu/simuv2/collide.cpp:163" + }, + { + "name": "yawKickCap", + "value": 6, + "unit": "radians per second", + "source": "src/modules/simu/simuv2/collide.cpp:164" + }, + { + "name": "impactDamageShare", + "value": 0.5, + "unit": "damage per squared meter per second", + "source": "src/modules/simu/simuv2/collide.cpp:173" + }, + { + "name": "scrapeDamageShare", + "value": 0.005, + "unit": "damage per meter per second", + "source": "src/modules/simu/simuv2/collide.cpp:173" + } + ], + "sources": [ + { + "path": "src/modules/simu/simuv2/collide.cpp", + "line": 123 + }, + { + "path": "src/modules/simu/simuv2/collide.cpp", + "line": 138 + }, + { + "path": "src/modules/simu/simuv2/collide.cpp", + "line": 158 + }, + { + "path": "src/modules/simu/simuv2/collide.cpp", + "line": 165 + }, + { + "path": "src/modules/simu/simuv2/collide.cpp", + "line": 179 + }, + { + "path": "src/libs/raceengineclient/raceengine.cpp", + "line": 436 + } + ], + "capabilities": [ + "wall collision", + "rebound", + "damage" + ], + "tags": [ + "collision", + "damage" + ] + }, + { + "id": "car-contact", + "node": "collisions", + "name": "Car-to-car impulse contact", + "summary": "Cars that overlap separate a few centimeters and exchange an elastic impulse, with extra damage for frontal hits", + "behavior": "A collision library reports the contact points between car shapes. The two cars are pushed apart along the contact normal by up to 0.05 meters, and no impulse applies when they already move apart. The impulse uses a restitution of 1 and acts on both linear and yaw velocity through the inverse mass and yaw inertia. Damage is 0.1 times the impulse magnitude, scaled by 1.5 when the hit lands within 60 degrees of the car nose, and by the rules and skill factors. A similar impulse handles solid wall shapes, with damage on the square of the impulse.", + "constants": [ + { + "name": "separationCap", + "value": 0.05, + "unit": "meters per simulation step at 500 Hz", + "source": "src/modules/simu/simuv2/collide.cpp:285" + }, + { + "name": "restitution", + "value": 1, + "unit": "dimensionless", + "source": "src/modules/simu/simuv2/collide.cpp:312" + }, + { + "name": "carDamageScale", + "value": 0.1, + "unit": "damage points per newton second", + "source": "src/modules/simu/simuv2/collide.cpp:22" + }, + { + "name": "frontalDamageFactor", + "value": 1.5, + "unit": "multiplier", + "source": "src/modules/simu/simuv2/collide.cpp:328" + }, + { + "name": "wallImpulseDamage", + "value": 0.00002, + "unit": "damage points per squared newton second", + "source": "src/modules/simu/simuv2/collide.cpp:459" + } + ], + "sources": [ + { + "path": "src/modules/simu/simuv2/collide.cpp", + "line": 285 + }, + { + "path": "src/modules/simu/simuv2/collide.cpp", + "line": 314 + }, + { + "path": "src/modules/simu/simuv2/collide.cpp", + "line": 326 + }, + { + "path": "src/modules/simu/simuv2/collide.cpp", + "line": 335 + }, + { + "path": "src/modules/simu/simuv2/collide.cpp", + "line": 461 + } + ], + "capabilities": [ + "car collision", + "impulse response", + "damage" + ], + "tags": [ + "collision", + "racing" + ] + }, + { + "id": "ai-corner-speed", + "node": "robot-driver", + "name": "Robot corner speed", + "summary": "The robot derives a safe speed for each track segment from its radius, friction and aero downforce", + "behavior": "At race start the robot assigns each straight an infinite radius and each turn its radius plus half the width, divided by how much of a quarter turn the whole corner covers, so short kinks get a larger effective radius. The allowed speed is the square root of friction times gravity times radius over one minus radius times downforce coefficient times friction over mass. The friction is the surface friction times the weakest tyre mu times a safety factor, 0.69 by default. A learning module adjusts the radius lap by lap. The robot uses full throttle while the allowed speed exceeds its speed by more than 1 meter per second, and otherwise holds the throttle that matches the allowed speed in the current gear.", + "constants": [ + { + "name": "defaultMuFactor", + "value": 0.69, + "unit": "friction multiplier", + "source": "src/drivers/bt/driver.cpp:120" + }, + { + "name": "fullThrottleMargin", + "value": 1, + "unit": "meters per second", + "source": "src/drivers/bt/driver.cpp:27" + }, + { + "name": "gravity", + "value": 9.81, + "unit": "meters per second squared", + "source": "src/drivers/bt/driver.cpp:26" + } + ], + "sources": [ + { + "path": "src/drivers/bt/driver.cpp", + "line": 243 + }, + { + "path": "src/drivers/bt/driver.cpp", + "line": 249 + }, + { + "path": "src/drivers/bt/driver.cpp", + "line": 277 + }, + { + "path": "src/drivers/bt/driver.cpp", + "line": 297 + }, + { + "path": "src/drivers/bt/driver.cpp", + "line": 302 + } + ], + "capabilities": [ + "corner speed", + "track radius", + "downforce aware" + ], + "tags": [ + "ai", + "speed", + "racing" + ] + }, + { + "id": "ai-braking", + "node": "robot-driver", + "name": "Robot braking distance", + "summary": "The robot looks ahead by its stopping distance and brakes fully when a slower segment comes within aero-aware braking range", + "behavior": "If the car goes faster than the current segment allows, brake rises by the speed excess over 1 meter per second. Otherwise the robot walks ahead segment by segment up to the distance needed to stop on the current friction. For each slower segment it computes the closed-form braking distance with drag and downforce, the log of the ratio of friction plus aero terms at the two speeds. It brakes fully when that distance exceeds the gap. Brake pedal is then scaled by the current grip over the grip at 84 meters per second, so braking is gentler at low speed where downforce is small. Opponent and pit filters can also force full braking.", + "constants": [ + { + "name": "brakeScaleSpeed", + "value": 84, + "unit": "meters per second", + "source": "src/drivers/bt/driver.cpp:44" + }, + { + "name": "reverseBrakeSpeed", + "value": 5, + "unit": "meters per second", + "source": "src/drivers/bt/driver.cpp:24" + } + ], + "sources": [ + { + "path": "src/drivers/bt/driver.cpp", + "line": 334 + }, + { + "path": "src/drivers/bt/driver.cpp", + "line": 338 + }, + { + "path": "src/drivers/bt/driver.cpp", + "line": 346 + }, + { + "path": "src/drivers/bt/driver.cpp", + "line": 1010 + }, + { + "path": "src/drivers/bt/driver.cpp", + "line": 736 + }, + { + "path": "src/drivers/bt/driver.cpp", + "line": 808 + } + ], + "capabilities": [ + "braking point", + "lookahead", + "aero braking" + ], + "tags": [ + "ai", + "braking" + ] + }, + { + "id": "ai-steering-overtake", + "node": "robot-driver", + "name": "Robot steering and overtaking", + "summary": "The robot steers at a point a speed-scaled distance ahead, shifted sideways to pass slower cars and let faster ones by", + "behavior": "The aim point is 17 meters plus 0.33 seconds of travel ahead along the track center, never closer than the previous point minus travel, and steering is the yaw error over the steering lock. A lateral offset moves the aim point at 5 meters per second, faster at low speed by up to 5 times. When a slower car ahead is within the catch distance, capped at 10 times its gap, the robot moves to the side that the opponent leaves open. With the opponent centered, it picks the inside of the next corner, keeping 0.5 meters from the edge of the usable third of the width. A lapping car behind for more than 5 seconds makes the robot move aside and lift to half throttle. A car alongside closer than 3 meters blends in a steer away from it.", + "constants": [ + { + "name": "lookaheadBase", + "value": 17, + "unit": "meters", + "source": "src/drivers/bt/driver.cpp:35" + }, + { + "name": "lookaheadPerSpeed", + "value": 0.33, + "unit": "seconds", + "source": "src/drivers/bt/driver.cpp:36" + }, + { + "name": "offsetSpeed", + "value": 5, + "unit": "meters per second", + "source": "src/drivers/bt/driver.cpp:40" + }, + { + "name": "catchFactor", + "value": 10, + "unit": "multiplier on gap", + "source": "src/drivers/bt/driver.cpp:50" + }, + { + "name": "letPassWait", + "value": 5, + "unit": "seconds", + "source": "src/drivers/bt/opponent.cpp:31" + }, + { + "name": "sideMargin", + "value": 3, + "unit": "meters", + "source": "src/drivers/bt/driver.cpp:38" + }, + { + "name": "letPassThrottle", + "value": 0.5, + "unit": "throttle fraction", + "source": "src/drivers/bt/driver.cpp:316" + } + ], + "sources": [ + { + "path": "src/drivers/bt/driver.cpp", + "line": 445 + }, + { + "path": "src/drivers/bt/driver.cpp", + "line": 390 + }, + { + "path": "src/drivers/bt/driver.cpp", + "line": 527 + }, + { + "path": "src/drivers/bt/driver.cpp", + "line": 546 + }, + { + "path": "src/drivers/bt/driver.cpp", + "line": 609 + }, + { + "path": "src/drivers/bt/opponent.cpp", + "line": 126 + }, + { + "path": "src/drivers/bt/driver.cpp", + "line": 869 + } + ], + "capabilities": [ + "path following", + "overtaking", + "blue flags" + ], + "tags": [ + "ai", + "steering", + "racing" + ] + }, + { + "id": "ai-driver-aids", + "node": "robot-driver", + "name": "Robot ABS, traction control and shifting", + "summary": "The robot trims brake and throttle on wheel slip, shifts near redline with hysteresis and launches with a timed clutch", + "behavior": "Above 3 meters per second, brake is reduced by slip beyond 2 meters per second over a 5 meter per second range, using average wheel speed against car speed. Throttle is reduced by driven-wheel slip beyond 2 meters per second over a 10 meter per second range. Throttle is also cut when the car runs wide of the usable track on a straight or the outside of a turn. The robot upshifts once the speed passes 90 percent of the redline speed in the current gear, and downshifts only when the lower gear would stay 4 meters per second clear of that point. In first gear the clutch closes over 2 seconds, faster once engine speed passes half of redline.", + "constants": [ + { + "name": "absMinSpeed", + "value": 3, + "unit": "meters per second", + "source": "src/drivers/bt/driver.cpp:32" + }, + { + "name": "absSlip", + "value": 2, + "unit": "meters per second", + "source": "src/drivers/bt/driver.cpp:30" + }, + { + "name": "absRange", + "value": 5, + "unit": "meters per second", + "source": "src/drivers/bt/driver.cpp:31" + }, + { + "name": "tclRange", + "value": 10, + "unit": "meters per second", + "source": "src/drivers/bt/driver.cpp:34" + }, + { + "name": "shiftPoint", + "value": 0.9, + "unit": "fraction of redline speed", + "source": "src/drivers/bt/driver.cpp:28" + }, + { + "name": "shiftHysteresis", + "value": 4, + "unit": "meters per second", + "source": "src/drivers/bt/driver.cpp:29" + }, + { + "name": "launchClutchTime", + "value": 2, + "unit": "seconds", + "source": "src/drivers/bt/driver.cpp:51" + } + ], + "sources": [ + { + "path": "src/drivers/bt/driver.cpp", + "line": 901 + }, + { + "path": "src/drivers/bt/driver.cpp", + "line": 919 + }, + { + "path": "src/drivers/bt/driver.cpp", + "line": 368 + }, + { + "path": "src/drivers/bt/driver.cpp", + "line": 373 + }, + { + "path": "src/drivers/bt/driver.cpp", + "line": 403 + }, + { + "path": "src/drivers/bt/driver.cpp", + "line": 978 + } + ], + "capabilities": [ + "abs", + "traction control", + "auto gearbox", + "launch" + ], + "tags": [ + "ai", + "traction", + "gears" + ] + }, + { + "id": "ai-stuck-recovery", + "node": "robot-driver", + "name": "Robot stuck recovery", + "summary": "A robot that sits slow, sideways and off the center line for two seconds reverses out with counter-steer", + "behavior": "The robot counts as stuck when its angle to the track exceeds 15 degrees, it moves slower than 5 meters per second and it sits more than 3 meters from the center line. Once that holds for 2 seconds, counted in robot updates, and the car points away from the center, it selects reverse with full throttle, no brake and the clutch closed. It steers by the negative of its track angle over the steering lock until the condition clears. Rolling backward faster than 5 meters per second in the normal state triggers full brake.", + "constants": [ + { + "name": "stuckAngle", + "value": 15, + "unit": "degrees", + "source": "src/drivers/bt/driver.cpp:22" + }, + { + "name": "stuckTime", + "value": 2, + "unit": "seconds", + "source": "src/drivers/bt/driver.cpp:23" + }, + { + "name": "stuckSpeed", + "value": 5, + "unit": "meters per second", + "source": "src/drivers/bt/driver.cpp:24" + }, + { + "name": "stuckOffCenter", + "value": 3, + "unit": "meters", + "source": "src/drivers/bt/driver.cpp:25" + } + ], + "sources": [ + { + "path": "src/drivers/bt/driver.cpp", + "line": 674 + }, + { + "path": "src/drivers/bt/driver.cpp", + "line": 677 + }, + { + "path": "src/drivers/bt/driver.cpp", + "line": 179 + }, + { + "path": "src/drivers/bt/driver.cpp", + "line": 128 + } + ], + "capabilities": [ + "stuck detection", + "reverse recovery" + ], + "tags": [ + "ai", + "recovery" + ] + }, + { + "id": "ai-opponent-awareness", + "node": "robot-driver", + "name": "Robot opponent classification", + "summary": "Each robot update tags every other car as ahead, behind, alongside, faster ahead, colliding or asking to pass", + "behavior": "Opponent distance is measured along the track from the start line and wrapped to half a lap. Only cars between 70 meters behind and 200 meters ahead count. A slower car ahead is tagged as ahead, its gap reduced by the longer car length and a 3 meter margin, and measured to its nearest corner once under 12 meters. It is also tagged colliding when the lateral gap falls under 1 meter. A faster car behind within 5 meters per second is tagged as behind, an overlapping car as alongside, and a faster car ahead separately. A lapping car builds a timer while behind or alongside, which resets to minus 30 seconds when it falls ahead.", + "constants": [ + { + "name": "frontRange", + "value": 200, + "unit": "meters", + "source": "src/drivers/bt/opponent.cpp:25" + }, + { + "name": "backRange", + "value": 70, + "unit": "meters", + "source": "src/drivers/bt/opponent.cpp:26" + }, + { + "name": "lengthMargin", + "value": 3, + "unit": "meters", + "source": "src/drivers/bt/opponent.cpp:27" + }, + { + "name": "sideCollisionMargin", + "value": 1, + "unit": "meters", + "source": "src/drivers/bt/opponent.cpp:28" + }, + { + "name": "exactDistance", + "value": 12, + "unit": "meters", + "source": "src/drivers/bt/opponent.cpp:29" + }, + { + "name": "lappedPenalty", + "value": -30, + "unit": "seconds", + "source": "src/drivers/bt/opponent.cpp:30" + } + ], + "sources": [ + { + "path": "src/drivers/bt/opponent.cpp", + "line": 55 + }, + { + "path": "src/drivers/bt/opponent.cpp", + "line": 64 + }, + { + "path": "src/drivers/bt/opponent.cpp", + "line": 101 + }, + { + "path": "src/drivers/bt/opponent.cpp", + "line": 106 + }, + { + "path": "src/drivers/bt/opponent.cpp", + "line": 150 + } + ], + "capabilities": [ + "opponent tracking", + "collision prediction" + ], + "tags": [ + "ai", + "racing" + ] + }, + { + "id": "laps-standings", + "node": "race-manager", + "name": "Lap counting and standings", + "summary": "Laps count on crossing the start line forward, a backward-crossing guard prevents cheats, and standings sort by distance raced", + "behavior": "A lap counts when a car moves from the last segment into the first segment. Crossing the other way raises a guard counter that a later forward crossing only clears, so reversing over the line earns nothing. Each lap records the lap time, a best lap that rule breaks can void, and gaps to the leader and to the car ahead. Distance raced is completed laps times track length plus distance from the line. Every step an insertion sort orders unfinished cars by distance raced. The race moves to finishing when the leader runs out of laps, and ends when every car has finished.", + "constants": [ + { + "name": "practiceSpeedReport", + "value": 3.6, + "unit": "kilometers per hour per meter per second", + "source": "src/libs/raceengineclient/raceengine.cpp:286" + } + ], + "sources": [ + { + "path": "src/libs/raceengineclient/raceengine.cpp", + "line": 242 + }, + { + "path": "src/libs/raceengineclient/raceengine.cpp", + "line": 245 + }, + { + "path": "src/libs/raceengineclient/raceengine.cpp", + "line": 248 + }, + { + "path": "src/libs/raceengineclient/raceengine.cpp", + "line": 262 + }, + { + "path": "src/libs/raceengineclient/raceengine.cpp", + "line": 360 + }, + { + "path": "src/libs/raceengineclient/raceengine.cpp", + "line": 382 + }, + { + "path": "src/libs/raceengineclient/raceengine.cpp", + "line": 308 + } + ], + "capabilities": [ + "lap timing", + "standings", + "race finish" + ], + "tags": [ + "race", + "rules", + "ui" + ] + }, + { + "id": "race-rules", + "node": "race-manager", + "name": "Race rules and penalties", + "summary": "Robots that lap far too slowly retire, corner cuts void laps or add time, and pit-lane speeding earns a drive-through", + "behavior": "A robot whose current lap time exceeds 84.5 seconds plus one second per 10 meters of track length is eliminated. In a turn, a car whose inner track edge distance drops below minus 0.7 of its width counts as cutting. Cutting voids the lap time when that rule is on. In a race it adds penalty time from speed, step length and depth over the inner radius. At the hardest skill level, speeding in the pit lane queues a drive-through penalty, and entering or leaving the pit lane away from its entry and exit queues a stop-and-go. The car is eliminated if it does not serve that penalty within 5 laps.", + "constants": [ + { + "name": "slowLapBase", + "value": 84.5, + "unit": "seconds", + "source": "src/libs/raceengineclient/raceengine.cpp:420" + }, + { + "name": "slowLapPerLength", + "value": 10, + "unit": "meters of track per extra second", + "source": "src/libs/raceengineclient/raceengine.cpp:420" + }, + { + "name": "cuttingLimit", + "value": 0.7, + "unit": "fraction of car width", + "source": "src/libs/raceengineclient/raceengine.cpp:479" + }, + { + "name": "penaltyLapWindow", + "value": 5, + "unit": "laps", + "source": "src/libs/raceengineclient/raceengine.cpp:607" + } + ], + "sources": [ + { + "path": "src/libs/raceengineclient/raceengine.cpp", + "line": 423 + }, + { + "path": "src/libs/raceengineclient/raceengine.cpp", + "line": 480 + }, + { + "path": "src/libs/raceengineclient/raceengine.cpp", + "line": 491 + }, + { + "path": "src/libs/raceengineclient/raceengine.cpp", + "line": 600 + }, + { + "path": "src/libs/raceengineclient/raceengine.cpp", + "line": 504 + } + ], + "capabilities": [ + "penalties", + "track limits", + "pit speed limit" + ], + "tags": [ + "race", + "rules" + ] + }, + { + "id": "pit-stop", + "node": "race-manager", + "name": "Pit stop service", + "summary": "A car that asked to pit stops in its box below 1 meter per second, and service time adds base, fuel, repair and penalty time", + "behavior": "Once a car has requested a stop, it enters service only inside its pit window, close enough to the pit wall, and with both speed components under 1 meter per second. The driver or robot then chooses fuel and repair. Service time is the base stop time plus fuel over the refuel rate plus repair points times the repair factor plus pending penalty time. A full tyre change adds time at the hardest skill level with tyre rules on. Setup changes are applied to the car when service starts. A stop-and-go penalty holds the car only for the penalty time.", + "constants": [ + { + "name": "pitStopSpeed", + "value": 1, + "unit": "meters per second", + "source": "src/libs/raceengineclient/raceengine.cpp:209" + } + ], + "sources": [ + { + "path": "src/libs/raceengineclient/raceengine.cpp", + "line": 190 + }, + { + "path": "src/libs/raceengineclient/raceengine.cpp", + "line": 208 + }, + { + "path": "src/libs/raceengineclient/raceengine.cpp", + "line": 59 + }, + { + "path": "src/libs/raceengineclient/raceengine.cpp", + "line": 63 + }, + { + "path": "src/libs/raceengineclient/raceengine.cpp", + "line": 75 + }, + { + "path": "src/libs/raceengineclient/raceengine.cpp", + "line": 78 + } + ], + "capabilities": [ + "pit stops", + "refuel", + "repair" + ], + "tags": [ + "race", + "strategy" + ] + } + ] +} diff --git a/data/games/vdrift.json b/data/games/vdrift.json new file mode 100644 index 0000000..857ddd1 --- /dev/null +++ b/data/games/vdrift.json @@ -0,0 +1,2934 @@ +{ + "version": 1, + "id": "vdrift", + "title": "VDrift", + "repo": "https://github.com/VDrift/vdrift", + "sha": "f835edaef1abe586b8d5bebeaa8c41f85e6dbea4", + "nodes": [ + { + "id": "car-dynamics", + "name": "Car dynamics update", + "purpose": "The per-tick vehicle update: wheel contact rays, suspension travel, driveline and tyre impulses, driver aids, aerodynamics and the rollover recovery path, all applied to a single rigid body with four ray-sprung wheels", + "paths": [ + "src/physics/cardynamics.cpp", + "src/physics/cardynamics.h" + ], + "functions": [ + { + "name": "CarDynamics::updateAction", + "path": "src/physics/cardynamics.cpp", + "lines": [ + 1182, + 1225 + ] + }, + { + "name": "CarDynamics::UpdateDriveline", + "path": "src/physics/cardynamics.cpp", + "lines": [ + 1452, + 1516 + ] + }, + { + "name": "CarDynamics::UpdateWheelContacts", + "path": "src/physics/cardynamics.cpp", + "lines": [ + 1227, + 1244 + ] + }, + { + "name": "CarDynamics::SetupWheelConstraints", + "path": "src/physics/cardynamics.cpp", + "lines": [ + 1325, + 1390 + ] + }, + { + "name": "CarDynamics::UpdateWheelConstraints", + "path": "src/physics/cardynamics.cpp", + "lines": [ + 1432, + 1450 + ] + }, + { + "name": "CarDynamics::UpdateSuspension", + "path": "src/physics/cardynamics.cpp", + "lines": [ + 1164, + 1179 + ] + }, + { + "name": "CarDynamics::ApplyABS", + "path": "src/physics/cardynamics.cpp", + "lines": [ + 1124, + 1149 + ] + }, + { + "name": "CarDynamics::ApplyTCS", + "path": "src/physics/cardynamics.cpp", + "lines": [ + 1094, + 1122 + ] + }, + { + "name": "CarDynamics::ApplyAerodynamics", + "path": "src/physics/cardynamics.cpp", + "lines": [ + 1076, + 1092 + ] + }, + { + "name": "CarDynamics::SetSteering", + "path": "src/physics/cardynamics.cpp", + "lines": [ + 1745, + 1765 + ] + }, + { + "name": "CarDynamics::AlignWithGround", + "path": "src/physics/cardynamics.cpp", + "lines": [ + 702, + 726 + ] + }, + { + "name": "CarDynamics::RolloverRecover", + "path": "src/physics/cardynamics.cpp", + "lines": [ + 1813, + 1834 + ] + }, + { + "name": "CarDynamics::AutoClutch", + "path": "src/physics/cardynamics.cpp", + "lines": [ + 1579, + 1613 + ] + }, + { + "name": "CarDynamics::NextGear", + "path": "src/physics/cardynamics.cpp", + "lines": [ + 1630, + 1651 + ] + }, + { + "name": "CarDynamics::GetTireSqueal", + "path": "src/physics/cardynamics.cpp", + "lines": [ + 992, + 1004 + ] + }, + { + "name": "CarDynamics::GetMaxSpeed", + "path": "src/physics/cardynamics.cpp", + "lines": [ + 1006, + 1014 + ] + }, + { + "name": "CarDynamics::GetBrakeDistance", + "path": "src/physics/cardynamics.cpp", + "lines": [ + 1016, + 1032 + ] + } + ], + "dependsOn": [] + }, + { + "id": "tyre-model", + "name": "Tyre force model", + "purpose": "Slip ratio and slip angle from wheel and contact velocities, then longitudinal, lateral and self-aligning forces from a magic-formula curve family, including camber shift, combined-slip scaling and a precomputed table of peak-force slip values", + "paths": [ + "src/physics/cartire1.cpp", + "src/physics/cartirebase.h", + "src/physics/cartire.h" + ], + "functions": [ + { + "name": "CarTire1::ComputeState", + "path": "src/physics/cartire1.cpp", + "lines": [ + 46, + 90 + ] + }, + { + "name": "CarTire1::ComputeAligningTorque", + "path": "src/physics/cartire1.cpp", + "lines": [ + 92, + 106 + ] + }, + { + "name": "CarTire1::getRollingResistance", + "path": "src/physics/cartire1.cpp", + "lines": [ + 108, + 118 + ] + }, + { + "name": "CarTire1::PacejkaFx", + "path": "src/physics/cartire1.cpp", + "lines": [ + 232, + 266 + ] + }, + { + "name": "CarTire1::PacejkaFy", + "path": "src/physics/cartire1.cpp", + "lines": [ + 268, + 306 + ] + }, + { + "name": "CarTire1::PacejkaMz", + "path": "src/physics/cartire1.cpp", + "lines": [ + 308, + 345 + ] + }, + { + "name": "CarTire1::PacejkaGx", + "path": "src/physics/cartire1.cpp", + "lines": [ + 347, + 357 + ] + }, + { + "name": "CarTire1::PacejkaGy", + "path": "src/physics/cartire1.cpp", + "lines": [ + 359, + 369 + ] + }, + { + "name": "FindPacejkaMax", + "path": "src/physics/cartire1.cpp", + "lines": [ + 372, + 390 + ] + }, + { + "name": "CarTire1::findIdealSlip", + "path": "src/physics/cartire1.cpp", + "lines": [ + 392, + 404 + ] + }, + { + "name": "CarTire1::initSlipLUT", + "path": "src/physics/cartire1.cpp", + "lines": [ + 406, + 413 + ] + }, + { + "name": "ComputeSlip", + "path": "src/physics/cartirebase.h", + "lines": [ + 23, + 31 + ] + }, + { + "name": "ComputeCamberAngle", + "path": "src/physics/cartirebase.h", + "lines": [ + 9, + 13 + ] + }, + { + "name": "ComputeCamberVelocity", + "path": "src/physics/cartirebase.h", + "lines": [ + 16, + 20 + ] + } + ], + "dependsOn": [] + }, + { + "id": "suspension", + "name": "Suspension and wheel kinematics", + "purpose": "Coilover springs and dampers, travel limits and overtravel, steering axis with caster and Ackermann, toe and camber offsets, and the sampled hinge kinematics for double-wishbone, strut and simple trailing-arm layouts", + "paths": [ + "src/physics/carsuspension.cpp", + "src/physics/carsuspension.h", + "src/suspensionbumpdetection.cpp", + "src/suspensionbumpdetection.h" + ], + "functions": [ + { + "name": "CarSuspension::Init", + "path": "src/physics/carsuspension.cpp", + "lines": [ + 58, + 73 + ] + }, + { + "name": "CarSuspension::SetSteering", + "path": "src/physics/carsuspension.cpp", + "lines": [ + 75, + 89 + ] + }, + { + "name": "CarSuspension::SetDisplacement", + "path": "src/physics/carsuspension.cpp", + "lines": [ + 91, + 110 + ] + }, + { + "name": "CarSuspension::UpdateDisplacement", + "path": "src/physics/carsuspension.cpp", + "lines": [ + 112, + 132 + ] + }, + { + "name": "ComputeTransforms", + "path": "src/physics/carsuspension.cpp", + "lines": [ + 268, + 275 + ] + }, + { + "name": "LoadCoilover", + "path": "src/physics/carsuspension.cpp", + "lines": [ + 291, + 302 + ] + }, + { + "name": "CarSuspension::Load", + "path": "src/physics/carsuspension.cpp", + "lines": [ + 304, + 379 + ] + }, + { + "name": "SuspensionBumpDetection::Update", + "path": "src/suspensionbumpdetection.cpp", + "lines": [ + 37, + 93 + ] + } + ], + "dependsOn": [] + }, + { + "id": "drivetrain", + "name": "Engine, clutch, gearbox and differential", + "purpose": "Engine torque from an interpolated curve with friction and rev limiting, clutch torque capacity, gear ratios and shift timing, and the limited-slip differential ratios that feed the wheel shafts", + "paths": [ + "src/physics/carengine.cpp", + "src/physics/carengine.h", + "src/physics/carclutch.h", + "src/physics/cartransmission.h", + "src/physics/cardifferential.h", + "src/physics/carwheel.h", + "src/physics/driveshaft.h" + ], + "functions": [ + { + "name": "CarEngineInfo::Load", + "path": "src/physics/carengine.cpp", + "lines": [ + 45, + 149 + ] + }, + { + "name": "CarEngineInfo::GetTorque", + "path": "src/physics/carengine.cpp", + "lines": [ + 156, + 160 + ] + }, + { + "name": "CarEngineInfo::GetFrictionTorque", + "path": "src/physics/carengine.cpp", + "lines": [ + 162, + 168 + ] + }, + { + "name": "CarEngine::Update", + "path": "src/physics/carengine.cpp", + "lines": [ + 191, + 229 + ] + }, + { + "name": "Init", + "path": "src/physics/carclutch.h", + "lines": [ + 38, + 41 + ] + }, + { + "name": "GetTorque", + "path": "src/physics/carclutch.h", + "lines": [ + 53, + 56 + ] + }, + { + "name": "Shift", + "path": "src/physics/cartransmission.h", + "lines": [ + 69, + 72 + ] + }, + { + "name": "GetClutchSpeed", + "path": "src/physics/cartransmission.h", + "lines": [ + 106, + 109 + ] + } + ], + "dependsOn": [] + }, + { + "id": "wheel-constraint", + "name": "Wheel contact constraints", + "purpose": "One three-row impulse constraint per wheel: a softened spring row for suspension travel, a rolling row along the wheel heading and a lateral row, limited by the tyre force budget and by the wheel shaft inertia; also the surface description those rows read", + "paths": [ + "src/physics/wheelconstraint.h", + "src/physics/collision_contact.h", + "src/physics/tracksurface.h", + "src/physics/carbrake.h" + ], + "functions": [ + { + "name": "solve", + "path": "src/physics/wheelconstraint.h", + "lines": [ + 39, + 45 + ] + }, + { + "name": "solveHard", + "path": "src/physics/wheelconstraint.h", + "lines": [ + 48, + 54 + ] + }, + { + "name": "initMassInertia", + "path": "src/physics/wheelconstraint.h", + "lines": [ + 56, + 63 + ] + }, + { + "name": "init", + "path": "src/physics/wheelconstraint.h", + "lines": [ + 92, + 98 + ] + }, + { + "name": "getContactVelocity", + "path": "src/physics/wheelconstraint.h", + "lines": [ + 100, + 106 + ] + }, + { + "name": "solveFriction", + "path": "src/physics/wheelconstraint.h", + "lines": [ + 108, + 126 + ] + }, + { + "name": "solveSuspension", + "path": "src/physics/wheelconstraint.h", + "lines": [ + 128, + 137 + ] + }, + { + "name": "CastRay", + "path": "src/physics/collision_contact.h", + "lines": [ + 98, + 114 + ] + } + ], + "dependsOn": [] + }, + { + "id": "physics-world", + "name": "Rigid-body world and surface ray casting", + "purpose": "The fixed-step rigid-body world the cars live in: substep budgeting, gravity, downward wheel ray queries that resolve a hit point, normal, penetration depth and a pointer to the track surface", + "paths": [ + "src/physics/dynamicsworld.cpp", + "src/physics/dynamicsworld.h" + ], + "functions": [ + { + "name": "DynamicsWorld::DynamicsWorld", + "path": "src/physics/dynamicsworld.cpp", + "lines": [ + 90, + 104 + ] + }, + { + "name": "DynamicsWorld::castRay", + "path": "src/physics/dynamicsworld.cpp", + "lines": [ + 116, + 184 + ] + }, + { + "name": "DynamicsWorld::update", + "path": "src/physics/dynamicsworld.cpp", + "lines": [ + 186, + 190 + ] + }, + { + "name": "DynamicsWorld::fractureCallback", + "path": "src/physics/dynamicsworld.cpp", + "lines": [ + 236, + 287 + ] + } + ], + "dependsOn": [] + }, + { + "id": "track-geometry", + "name": "Track strips, patches and surface queries", + "purpose": "Bezier road patches joined into strips with a bounding-volume index, ray intersection against a patch subdivided into a parameter quad tree, strip closure detection and sector bookkeeping", + "paths": [ + "src/track.cpp", + "src/track.h", + "src/roadpatch.cpp", + "src/roadstrip.cpp", + "src/bezier.cpp" + ], + "functions": [ + { + "name": "Track::CastRay", + "path": "src/track.cpp", + "lines": [ + 128, + 154 + ] + }, + { + "name": "RoadStrip::ReadFrom", + "path": "src/roadstrip.cpp", + "lines": [ + 29, + 94 + ] + }, + { + "name": "RoadStrip::GenerateSpacePartitioning", + "path": "src/roadstrip.cpp", + "lines": [ + 96, + 104 + ] + }, + { + "name": "RoadStrip::Collide", + "path": "src/roadstrip.cpp", + "lines": [ + 106, + 147 + ] + }, + { + "name": "RoadPatch::Collide", + "path": "src/roadpatch.cpp", + "lines": [ + 91, + 101 + ] + }, + { + "name": "Bezier::CollideSubDivQuadSimpleNorm", + "path": "src/bezier.cpp", + "lines": [ + 356, + 433 + ] + } + ], + "dependsOn": [] + }, + { + "id": "ai-drivers", + "name": "Opponent drivers", + "purpose": "Per-tick opponent logic: read the patch under the car, derive a corner speed limit from curvature and friction, run a braking lookahead along the strip, steer toward a lookahead point and rate-limit the resulting throttle and brake", + "paths": [ + "src/ai/ai_car_standard.cpp", + "src/ai/ai_car_standard.h", + "src/ai/ai.cpp", + "src/ai/ai_car.h" + ], + "functions": [ + { + "name": "AiCarStandard::RateLimit", + "path": "src/ai/ai_car_standard.cpp", + "lines": [ + 82, + 90 + ] + }, + { + "name": "AiCarStandard::Update", + "path": "src/ai/ai_car_standard.cpp", + "lines": [ + 92, + 97 + ] + }, + { + "name": "AiCarStandard::GetPatchRadius", + "path": "src/ai/ai_car_standard.cpp", + "lines": [ + 132, + 154 + ] + }, + { + "name": "AiCarStandard::RevisePatch", + "path": "src/ai/ai_car_standard.cpp", + "lines": [ + 198, + 265 + ] + }, + { + "name": "AiCarStandard::UpdateGasBrake", + "path": "src/ai/ai_car_standard.cpp", + "lines": [ + 267, + 391 + ] + }, + { + "name": "AiCarStandard::CalcSpeedLimit", + "path": "src/ai/ai_car_standard.cpp", + "lines": [ + 393, + 411 + ] + }, + { + "name": "AiCarStandard::UpdateSteer", + "path": "src/ai/ai_car_standard.cpp", + "lines": [ + 413, + 509 + ] + }, + { + "name": "AiCarStandard::BrakeFromOthers", + "path": "src/ai/ai_car_standard.cpp", + "lines": [ + 525, + 576 + ] + }, + { + "name": "AiCarStandard::AnalyzeOthers", + "path": "src/ai/ai_car_standard.cpp", + "lines": [ + 578, + 641 + ] + }, + { + "name": "AiCarStandard::SteerAwayFromOthers", + "path": "src/ai/ai_car_standard.cpp", + "lines": [ + 643, + 680 + ] + } + ], + "dependsOn": [ + "track-geometry", + "car-dynamics" + ] + }, + { + "id": "racing-line", + "parent": "ai-drivers", + "name": "Generated racing line", + "purpose": "An offline pass over the road strip that nudges a lateral lane value at every sample until the curvature is minimised, producing the reference path and the per-sample curvature used by the drivers and the drawn racing line", + "paths": [ + "src/k1999.cpp", + "src/k1999.h", + "src/trackmap.cpp" + ], + "functions": [ + { + "name": "K1999::UpdateTxTy", + "path": "src/k1999.cpp", + "lines": [ + 45, + 49 + ] + }, + { + "name": "K1999::GetRInverse", + "path": "src/k1999.cpp", + "lines": [ + 72, + 89 + ] + }, + { + "name": "K1999::AdjustRadius", + "path": "src/k1999.cpp", + "lines": [ + 94, + 169 + ] + }, + { + "name": "K1999::Smooth", + "path": "src/k1999.cpp", + "lines": [ + 174, + 207 + ] + }, + { + "name": "K1999::CalcRaceLine", + "path": "src/k1999.cpp", + "lines": [ + 246, + 280 + ] + } + ], + "dependsOn": [ + "track-geometry" + ] + }, + { + "id": "race-timing", + "name": "Lap and sector timing", + "purpose": "Per-car lap and sector clocks, staging countdown, best-lap records keyed by car type, drift score accumulation and the running placement ordering used for the results screen", + "paths": [ + "src/timer.cpp", + "src/timer.h" + ], + "functions": [ + { + "name": "Timer::Load", + "path": "src/timer.cpp", + "lines": [ + 32, + 49 + ] + }, + { + "name": "Timer::Tick", + "path": "src/timer.cpp", + "lines": [ + 68, + 88 + ] + }, + { + "name": "Timer::Lap", + "path": "src/timer.cpp", + "lines": [ + 90, + 117 + ] + }, + { + "name": "Timer::UpdateDistance", + "path": "src/timer.cpp", + "lines": [ + 119, + 123 + ] + }, + { + "name": "Timer::GetCarPlace", + "path": "src/timer.cpp", + "lines": [ + 146, + 165 + ] + } + ], + "dependsOn": [ + "track-geometry" + ] + }, + { + "id": "game-loop", + "name": "Fixed-step game loop", + "purpose": "The fixed logic tick that drives AI, car input, the physics world, car state consumers, track objects, timing, particles and force feedback in a fixed order, with a wall-clock catch-up budget", + "paths": [ + "src/game.cpp" + ], + "functions": [ + { + "name": "Game::Tick", + "path": "src/game.cpp", + "lines": [ + 802, + 843 + ] + }, + { + "name": "Game::AdvanceGameLogic", + "path": "src/game.cpp", + "lines": [ + 846, + 926 + ] + }, + { + "name": "Game::UpdateTimer", + "path": "src/game.cpp", + "lines": [ + 984, + 1054 + ] + }, + { + "name": "Game::UpdateCars", + "path": "src/game.cpp", + "lines": [ + 1285, + 1301 + ] + }, + { + "name": "Game::UpdateDriftScore", + "path": "src/game.cpp", + "lines": [ + 2595, + 2653 + ] + } + ], + "dependsOn": [ + "car-dynamics", + "ai-drivers", + "race-timing", + "physics-world" + ] + }, + { + "id": "camera", + "parent": "game-loop", + "name": "Camera rigs", + "purpose": "Chase and orbit camera behaviours: an offset frame carried by the car orientation, a blend toward the target position, a look-at orientation, and an orbit rig with clamped yaw, pitch and distance", + "paths": [ + "src/camera_chase.cpp", + "src/camera_chase.h", + "src/camera_orbit.cpp", + "src/camera_orbit.h" + ], + "functions": [ + { + "name": "CameraChase::CameraChase", + "path": "src/camera_chase.cpp", + "lines": [ + 22, + 28 + ] + }, + { + "name": "CameraChase::SetOffset", + "path": "src/camera_chase.cpp", + "lines": [ + 30, + 37 + ] + }, + { + "name": "CameraChase::Reset", + "path": "src/camera_chase.cpp", + "lines": [ + 39, + 46 + ] + }, + { + "name": "CameraChase::Update", + "path": "src/camera_chase.cpp", + "lines": [ + 48, + 59 + ] + }, + { + "name": "CameraOrbit::CameraOrbit", + "path": "src/camera_orbit.cpp", + "lines": [ + 23, + 31 + ] + }, + { + "name": "CameraOrbit::Reset", + "path": "src/camera_orbit.cpp", + "lines": [ + 42, + 50 + ] + }, + { + "name": "CameraOrbit::Rotate", + "path": "src/camera_orbit.cpp", + "lines": [ + 57, + 66 + ] + }, + { + "name": "CameraOrbit::Move", + "path": "src/camera_orbit.cpp", + "lines": [ + 68, + 79 + ] + } + ], + "dependsOn": [] + }, + { + "id": "controls", + "name": "Driver input mapping", + "purpose": "Binding-aware analogue shaping applied before the car sees any input: dead zone, gain, response exponent, invert flag, and a signed ramp that turns a button press into a gradual analogue axis", + "paths": [ + "src/carcontrolmap.cpp", + "src/carcontrolmap.h" + ], + "functions": [ + { + "name": "Ramp", + "path": "src/carcontrolmap.cpp", + "lines": [ + 34, + 53 + ] + }, + { + "name": "ApplyDeadzone", + "path": "src/carcontrolmap.cpp", + "lines": [ + 55, + 61 + ] + }, + { + "name": "ApplyGain", + "path": "src/carcontrolmap.cpp", + "lines": [ + 63, + 66 + ] + }, + { + "name": "ApplyExponent", + "path": "src/carcontrolmap.cpp", + "lines": [ + 68, + 71 + ] + }, + { + "name": "HandleAxis", + "path": "src/carcontrolmap.cpp", + "lines": [ + 550, + 562 + ] + }, + { + "name": "HandleButton", + "path": "src/carcontrolmap.cpp", + "lines": [ + 565, + 571 + ] + } + ], + "dependsOn": [] + }, + { + "id": "car-visuals", + "name": "Car and skid-mark visuals", + "purpose": "Interpolated body and wheel transforms taken from the physics read-back, steering and brake-light state, and the skid-mark emitters whose energy accumulates from tyre squeal intensity", + "paths": [ + "src/cargraphics.cpp", + "src/skidmarks.cpp", + "src/skidmarks.h" + ], + "functions": [ + { + "name": "CarGraphics::Update", + "path": "src/cargraphics.cpp", + "lines": [ + 355, + 366 + ] + }, + { + "name": "CarGraphics::Update", + "path": "src/cargraphics.cpp", + "lines": [ + 368, + 408 + ] + }, + { + "name": "SkidMarks::UpdateEmitter", + "path": "src/skidmarks.cpp", + "lines": [ + 86, + 149 + ] + } + ], + "dependsOn": [ + "car-dynamics" + ] + }, + { + "id": "replay", + "parent": "game-loop", + "name": "Replay recording and playback", + "purpose": "An input-delta stream plus periodic car state snapshots, replayed by feeding recorded inputs back into the same car dynamics so playback stays consistent with the live run", + "paths": [ + "src/replay.cpp", + "src/replay.h" + ], + "functions": [ + { + "name": "Replay::RecordFrame", + "path": "src/replay.cpp", + "lines": [ + 112, + 125 + ] + }, + { + "name": "Replay::CarState::RecordFrame", + "path": "src/replay.cpp", + "lines": [ + 127, + 156 + ] + } + ], + "dependsOn": [ + "car-dynamics" + ] + } + ], + "cards": [ + { + "id": "fixed-step-substep-budget", + "node": "game-loop", + "name": "Fixed logic tick and substep budget", + "summary": "A 90 Hz logic tick, a ten-substep rigid-body budget and ten internal driveline substeps with four solver passes", + "behavior": "Game logic advances in fixed steps rather than by variable frame time. The loop accumulates wall-clock time and runs one logic step per ninetieth of a second of simulated time, capping the number of catch-up steps it will attempt so that a slow frame degrades the frame rate instead of the simulation. Inside the physics world the step is handed to a solver that may split it into up to ten substeps, and inside the car each tyre-and-driveline pass is itself resolved in ten substeps with four impulse iterations per substep. The practical consequence for feel is that wheel spin, clutch lock and tyre force limits stay stable at speed, and a car that is pushed into a wall never explodes, because the cheapest resolution is always a fraction of the fixed tick.", + "constants": [ + { + "name": "logicTickRate", + "value": 90, + "unit": "logic steps per second (one step each 1/90 s of simulated time)", + "source": "src/game.cpp:96" + }, + { + "name": "minimumFrameRate", + "value": 10, + "unit": "frames per second before simulated time is discarded, evaluated once per 1/90 s tick", + "source": "src/game.cpp:805" + }, + { + "name": "maxRigidBodySubsteps", + "value": 10, + "unit": "substeps per 1/90 s logic tick", + "source": "src/physics/dynamicsworld.h:39" + }, + { + "name": "drivelineSubsteps", + "value": 10, + "unit": "substeps per 1/90 s logic tick, giving a 1/900 s internal step", + "source": "src/physics/cardynamics.cpp:40" + }, + { + "name": "solverIterations", + "value": 4, + "unit": "impulse iterations per substep, resolved every 1/900 s", + "source": "src/physics/cardynamics.cpp:1454" + }, + { + "name": "gravity", + "value": 9.81, + "unit": "metres per second squared used in grip and braking arithmetic, read once per 1/90 s tick", + "source": "src/physics/cardynamics.cpp:39" + } + ], + "sources": [ + { + "path": "src/game.cpp", + "line": 96 + }, + { + "path": "src/game.cpp", + "line": 819 + }, + { + "path": "src/game.cpp", + "line": 883 + }, + { + "path": "src/physics/dynamicsworld.cpp", + "line": 188 + }, + { + "path": "src/physics/cardynamics.cpp", + "line": 1484 + } + ], + "capabilities": [ + "fixed timestep", + "substepping", + "deterministic catch-up", + "solver iteration budget" + ], + "tags": [ + "physics", + "timing", + "stability", + "loop" + ] + }, + { + "id": "wheel-ray-contact", + "node": "car-dynamics", + "name": "Downward wheel rays and suspension travel", + "summary": "Four downward rays find the ground; penetration depth past the wheel radius becomes the suspension displacement for that tick", + "behavior": "Every tick four rays are cast downward from points offset along the body, each starting above the wheel hub by one wheel radius, and each is given four metres of reach. A hit supplies a contact point, a surface normal, a penetration depth and a pointer to the surface. The suspension is then driven by a displacement computed from the geometry rather than from the ray distance directly: twice the wheel radius minus the reported depth, so a wheel whose hub sits exactly one radius above the ground rests at zero travel and a wheel pressed into the ground gets positive travel. Any surface bump is added at half amplitude. This makes the whole car body a single rigid mass riding on four ray springs, so ground clearance, wheelbase and ride height come out of the ray geometry rather than from a special-case vehicle solver.", + "constants": [ + { + "name": "contactRayLength", + "value": 4, + "unit": "metres of downward reach per ray, cast once per 1/90 s logic tick", + "source": "src/physics/cardynamics.cpp:1230" + }, + { + "name": "radiusToDisplacementFactor", + "value": 2, + "unit": "wheel radii, giving travel = 2 x radius - penetration depth, once per 1/90 s tick", + "source": "src/physics/cardynamics.cpp:1177" + }, + { + "name": "surfaceBumpShare", + "value": 0.5, + "unit": "fraction of the surface bump amplitude added to travel, once per 1/90 s tick", + "source": "src/physics/cardynamics.cpp:1175" + } + ], + "sources": [ + { + "path": "src/physics/cardynamics.cpp", + "line": 1227 + }, + { + "path": "src/physics/cardynamics.cpp", + "line": 1233 + }, + { + "path": "src/physics/cardynamics.cpp", + "line": 1164 + }, + { + "path": "src/physics/collision_contact.h", + "line": 98 + } + ], + "capabilities": [ + "ray suspended body", + "ride height", + "ground normal", + "penetration depth" + ], + "tags": [ + "physics", + "suspension", + "collision", + "ground" + ] + }, + { + "id": "airborne-rollover-recovery", + "node": "physics-world", + "name": "Airborne wheels and rollover recovery", + "summary": "A wheel with no ground hit contributes no force, and a car tilted past roughly forty-five degrees can be righted and dropped back onto the road", + "behavior": "When a wheel ray finds nothing, the contact is still constructed but flagged as a non-contact with no surface and no hit, so that corner contributes no suspension force and no friction for that tick: the car is genuinely airborne from that wheel outward, and a jump therefore behaves like a thrown rigid body rather than a magnetised car. If the whole car leaves the road, all four corners fall into the same state and only gravity and aerodynamics act. Recovery is a separate explicit path. A reset places the car along the down axis until the lowest wheel is exactly one radius above the ground and zeroes both linear and angular velocity, which is what makes a respawn land cleanly. A rollover request compares the car up axis with the world up axis and only rotates the car when the tilt is at least forty-five degrees, so a car leaning on a kerb is not silently flipped back upright.", + "constants": [ + { + "name": "worldGravity", + "value": -9.81, + "unit": "metres per second squared on the world up axis, integrated every 1/900 s substep", + "source": "src/physics/dynamicsworld.cpp:102" + }, + { + "name": "rolloverRecoveryThreshold", + "value": 4, + "unit": "divisor of pi radians giving the tilt threshold (pi/4, about 45 degrees), checked per reset request", + "source": "src/physics/cardynamics.cpp:1825" + }, + { + "name": "resetGroundClearanceFactor", + "value": 2, + "unit": "wheel radii used when measuring the lowest wheel, evaluated per reset request", + "source": "src/physics/cardynamics.cpp:710" + } + ], + "sources": [ + { + "path": "src/physics/dynamicsworld.cpp", + "line": 116 + }, + { + "path": "src/physics/cardynamics.cpp", + "line": 1237 + }, + { + "path": "src/physics/cardynamics.cpp", + "line": 702 + }, + { + "path": "src/physics/cardynamics.cpp", + "line": 1813 + } + ], + "capabilities": [ + "airborne behaviour", + "no-contact wheels", + "ground alignment", + "rollover recovery", + "velocity reset" + ], + "tags": [ + "physics", + "jump", + "recovery", + "feel" + ] + }, + { + "id": "anti-roll-coupling", + "node": "car-dynamics", + "name": "Anti-roll bar coupling", + "summary": "Opposite wheels on an axle share a spring rate proportional to their travel difference, with a stiffness floor and a hard bump-stop term past full travel", + "behavior": "Wheels are handled in axle pairs. For each pair the model takes the difference between the two displacements and converts it into an added spring rate on the compressed side and a negative one on the extended side, scaled so that a car rolling into a corner stiffens the loaded corner and softens the unloaded one. The combined spring plus anti-roll rate is floored just above zero, so the extra term can never invert the spring and start pulling the body down. When a wheel runs past its published travel the excess is treated as overtravel: it is added to the displacement and a very stiff bump-stop term is folded into the spring rate, which produces a sharp, non-linear stop instead of the wheel sinking through the body.", + "constants": [ + { + "name": "wheelsPerAxlePair", + "value": 2, + "unit": "wheels sharing one anti-roll rate, recomputed once per 1/90 s tick", + "source": "src/physics/cardynamics.cpp:1329" + }, + { + "name": "antiRollRateDefault", + "value": 8000, + "unit": "newtons per metre of left-right travel difference, once per 1/90 s tick", + "source": "src/physics/carsuspension.cpp:28" + } + ], + "sources": [ + { + "path": "src/physics/cardynamics.cpp", + "line": 1325 + }, + { + "path": "src/physics/cardynamics.cpp", + "line": 1337 + }, + { + "path": "src/physics/cardynamics.cpp", + "line": 1360 + }, + { + "path": "src/physics/carsuspension.h", + "line": 99 + } + ], + "capabilities": [ + "anti-roll bars", + "axle coupling", + "stiffness floor", + "bump stop" + ], + "tags": [ + "physics", + "suspension", + "handling", + "roll" + ] + }, + { + "id": "steering-geometry-and-assist", + "node": "car-dynamics", + "name": "Steering lock, Ackermann and assist", + "summary": "Steering is a normalised command scaled by per-wheel lock, bent by an Ackermann term and optionally clamped to the tyre peak slip angle at speed", + "behavior": "Driver steering arrives as a single value between full left and full right. Each wheel multiplies it by its own lock angle, then bends the result through a per-wheel Ackermann term so the two front wheels can end up at different angles, with that geometry expressed through the steering axis rather than hard-coded per side. Toe and camber are baked into the same wheel orientation, and caster tilts the steering axis so lock also feeds a little camber. An optional assist builds a clamp ceiling that starts at the driver command and blends toward the steering angle that produces peak lateral grip, blending in with squared speed and reaching full effect at roughly ten metres per second, then clips the command into that ceiling. The effect is that a keyboard or pad player gets usable lock at parking speed and a self-limiting steer angle once the car is loaded up.", + "constants": [ + { + "name": "assistBlendPerSquaredSpeed", + "value": 0.01, + "unit": "per square metre per second squared, saturating at 10 m/s, evaluated once per 1/90 s tick", + "source": "src/physics/cardynamics.cpp:1754" + }, + { + "name": "degreesPerPiRadians", + "value": 180, + "unit": "degrees per pi radians, the conversion applied to setup toe, camber and caster once at load", + "source": "src/physics/carsuspension.cpp:24" + } + ], + "sources": [ + { + "path": "src/physics/cardynamics.cpp", + "line": 1745 + }, + { + "path": "src/physics/cardynamics.cpp", + "line": 1750 + }, + { + "path": "src/physics/carsuspension.cpp", + "line": 69 + }, + { + "path": "src/physics/carsuspension.cpp", + "line": 75 + } + ], + "capabilities": [ + "steering lock", + "Ackermann geometry", + "caster", + "toe", + "steering assist" + ], + "tags": [ + "handling", + "steering", + "assist", + "feel" + ] + }, + { + "id": "suspension-spring-damper", + "node": "suspension", + "name": "Coilover spring, damper and travel", + "summary": "Per-wheel spring rate, split compression and rebound damping, a travel limit and the rest-position offset implied by static wheel load", + "behavior": "Each corner carries a spring rate, a compression damping value, a rebound damping value, an anti-roll rate and a travel figure, all loaded from the car setup and all given defaults so a partially specified car still drives. The published travel is measured from the laden rest position, so at load time the model subtracts the static sag, computed as wheel load divided by spring rate, from zero and treats the rest as an offset inside the full mechanical range. That full range is what the wheel actually sweeps. Compression and rebound are separate setup numbers, though only the compression value is currently read back as the constraint damping, so rebound is a documented setup value without a consumer in this revision. The spring rate doubles as the stiffness fed to the suspension constraint row, so a soft car visibly rolls and pitches while a stiff one skips over bumps.", + "constants": [ + { + "name": "springRateDefault", + "value": 50000, + "unit": "newtons per metre, applied every 1/900 s driveline substep", + "source": "src/physics/carsuspension.cpp:27" + }, + { + "name": "compressionDampingDefault", + "value": 2500, + "unit": "newtons per metre per second of compression, applied every 1/900 s substep", + "source": "src/physics/carsuspension.cpp:29" + }, + { + "name": "reboundDampingDefault", + "value": 4000, + "unit": "newtons per metre per second of extension, loaded at car setup and not consumed by the current constraint path", + "source": "src/physics/carsuspension.cpp:30" + }, + { + "name": "travelFromRestDefault", + "value": 0.2, + "unit": "metres of compression travel from the laden rest position, a static setup value, no per-tick rate", + "source": "src/physics/carsuspension.cpp:31" + }, + { + "name": "unsprungInverseMassDefault", + "value": 0.05, + "unit": "inverse kilograms, a 20 kg wheel and hub, used each 1/900 s substep", + "source": "src/physics/carsuspension.cpp:37" + } + ], + "sources": [ + { + "path": "src/physics/carsuspension.cpp", + "line": 26 + }, + { + "path": "src/physics/carsuspension.cpp", + "line": 327 + }, + { + "path": "src/physics/carsuspension.cpp", + "line": 366 + }, + { + "path": "src/physics/carsuspension.h", + "line": 101 + } + ], + "capabilities": [ + "spring rate", + "compression damping", + "rebound damping", + "static sag", + "travel limit" + ], + "tags": [ + "physics", + "suspension", + "feel", + "setup" + ] + }, + { + "id": "suspension-kinematics-sampling", + "node": "suspension", + "name": "Sampled suspension kinematics", + "summary": "Hinge geometry is evaluated at five travel stations and interpolated at runtime, so camber and track change come from the linkage", + "behavior": "Instead of integrating a linkage every tick, the loader solves the suspension geometry analytically for a double-wishbone layout, a MacPherson strut or a simple trailing hinge, then records wheel position and orientation at five evenly spaced travel stations between full droop and full bump. At runtime the current displacement is scaled into that four-interval table and both position and orientation are mixed linearly between the two nearest stations. A wishbone corner is solved as a circle-circle intersection in the upper hinge plane, a strut corner as a rotation about the lower hinge with the upright axis realigned, and the simple corner as a rotation about a single hinge. The practical effect is a wheel that gains negative camber and shifts inboard as it compresses, without paying for a constraint chain.", + "constants": [ + { + "name": "travelStations", + "value": 5, + "unit": "sampled travel stations per wheel, solved once at car load", + "source": "src/physics/carsuspension.cpp:272" + }, + { + "name": "interpolationSegments", + "value": 4, + "unit": "interpolation intervals between the five stations, read once per 1/90 s tick", + "source": "src/physics/carsuspension.h:75" + } + ], + "sources": [ + { + "path": "src/physics/carsuspension.cpp", + "line": 267 + }, + { + "path": "src/physics/carsuspension.cpp", + "line": 342 + }, + { + "path": "src/physics/carsuspension.h", + "line": 66 + }, + { + "path": "src/physics/carsuspension.h", + "line": 73 + } + ], + "capabilities": [ + "double wishbone", + "MacPherson strut", + "trailing arm", + "camber gain", + "table interpolation" + ], + "tags": [ + "physics", + "suspension", + "geometry", + "visuals" + ] + }, + { + "id": "tyre-slip-and-forces", + "node": "tyre-model", + "name": "Slip ratio, slip angle and drive force", + "summary": "Slip is derived from wheel and contact velocities, then a magic-formula curve turns it into longitudinal and lateral force scaled by surface grip", + "behavior": "For each wheel the model compares the contact patch velocity with the wheel rotation speed. Longitudinal slip is the difference between rotational and forward speed over forward speed, with the divisor floored so a stationary car does not divide by zero; slip angle is the negative arctangent of lateral over forward speed. Load is converted to kilonewtons and capped so extreme suspension impulses cannot drive the curve into nonsense. Slip ratio is then rescaled by a hundred to express it in percent, which is the input convention the fitted coefficients expect, and the vertical load, camber and slip angle feed a magic-formula curve: a shape factor, a load-dependent peak, a low-slip stiffness that decays exponentially with load, a curvature factor and horizontal and vertical shifts. The resulting force is finally multiplied by the surface friction coefficient, so the same car keeps its full grip figure on asphalt and a smaller one on gravel.", + "constants": [ + { + "name": "slipPercentScale", + "value": 100, + "unit": "percent, converting slip ratio to the percent slip the curve is fitted in, each 1/900 s substep", + "source": "src/physics/cartire1.cpp:255" + }, + { + "name": "loadCeiling", + "value": 30, + "unit": "kilonewtons of vertical load before the curve is clamped, each 1/900 s substep", + "source": "src/physics/cartire1.cpp:60" + }, + { + "name": "radiansToDegrees", + "value": 180, + "unit": "degrees per pi radians, applied to slip angle and camber each 1/900 s substep", + "source": "src/physics/cartire1.cpp:26" + } + ], + "sources": [ + { + "path": "src/physics/cartire1.cpp", + "line": 46 + }, + { + "path": "src/physics/cartire1.cpp", + "line": 232 + }, + { + "path": "src/physics/cartirebase.h", + "line": 23 + }, + { + "path": "src/physics/cardynamics.cpp", + "line": 1370 + } + ], + "capabilities": [ + "slip ratio", + "slip angle", + "magic formula curve", + "load sensitivity", + "surface scaling" + ], + "tags": [ + "physics", + "tyres", + "grip", + "feel" + ] + }, + { + "id": "tyre-peak-slip-table", + "node": "tyre-model", + "name": "Peak-slip lookup table", + "summary": "Peak-force slip values are solved numerically at build time into a table indexed by wheel load, and used to normalise driver aids", + "behavior": "The slip value that produces peak force depends on load, so the model does not use a single tuned constant. Instead the curve coefficients are assembled for one load, the peak is found by a Newton-Raphson search that walks the curve until the curvature-adjusted tangent reaches a quarter turn, stopping when the correction becomes tiny or after a small fixed number of steps. The resulting slip ratio and slip angle are stored in a table with a five-hundred-newton load step and twenty entries. The ratio is scaled by a hundredth so the percent-domain peak becomes a ratio again, and the angle is converted from degrees to radians. Because the table is filled once, the aids that need \"the slip a fast driver would use\" can read it in constant time every substep, and a heavy rear-engined car automatically wants a different slip angle from a light front-driver.", + "constants": [ + { + "name": "tableLoadStep", + "value": 500, + "unit": "newtons per table row, built once at car load", + "source": "src/physics/cartirebase.h:52" + }, + { + "name": "tableEntries", + "value": 20, + "unit": "load rows, covering 500 N to 10 kN, read each 1/900 s substep", + "source": "src/physics/cartirebase.h:56" + }, + { + "name": "peakSlipRatioScale", + "value": 0.01, + "unit": "ratio per percent slip, applied when the table is filled at car load", + "source": "src/physics/cartire1.cpp:397" + }, + { + "name": "peakSearchIterations", + "value": 8, + "unit": "Newton-Raphson steps per load row, run once at car load", + "source": "src/physics/cartire1.cpp:378" + } + ], + "sources": [ + { + "path": "src/physics/cartire1.cpp", + "line": 372 + }, + { + "path": "src/physics/cartire1.cpp", + "line": 392 + }, + { + "path": "src/physics/cartire1.cpp", + "line": 406 + }, + { + "path": "src/physics/cartirebase.h", + "line": 36 + }, + { + "path": "src/physics/cardynamics.cpp", + "line": 1512 + } + ], + "capabilities": [ + "peak slip search", + "load-indexed table", + "runtime interpolation", + "aid reference values" + ], + "tags": [ + "physics", + "tyres", + "tuning", + "aids" + ] + }, + { + "id": "camber-thrust-approximation", + "node": "tyre-model", + "name": "Camber thrust and small-angle approximation", + "summary": "Camber is clamped, converted with a truncated series instead of an arcsine, and fed into both a lateral force shift and an extra slip velocity", + "behavior": "Camber enters the tyre in two ways. Inside the force curve it produces a horizontal shift, so a cambered tyre at zero slip angle still generates side force, and a vertical shift that biases the whole curve. Outside the curve, the model needs a slip velocity to represent the sideways drag a tilted wheel creates. Rather than calling an inverse sine it clamps the sine first and recovers the angle with a truncated cubic series, then uses a second truncated series for the tangent when it turns that angle into a velocity, so a car pitched onto two wheels cannot produce a wild value. The resulting slip velocity is added to the lateral row of the wheel constraint, which is what makes a leaning car wander and a well-cambered front end bite on turn-in.", + "constants": [ + { + "name": "camberSineClamp", + "value": 0.3, + "unit": "sine of camber angle, the clamp applied each 1/900 s substep", + "source": "src/physics/cartirebase.h:11" + }, + { + "name": "arcsineSeriesDivisor", + "value": 6, + "unit": "divisor of the cubic term in the arcsine series, each 1/900 s substep", + "source": "src/physics/cartirebase.h:12" + }, + { + "name": "tangentSeriesDivisor", + "value": 3, + "unit": "divisor of the cubic term in the tangent series, each 1/900 s substep", + "source": "src/physics/cartirebase.h:18" + } + ], + "sources": [ + { + "path": "src/physics/cartirebase.h", + "line": 9 + }, + { + "path": "src/physics/cartirebase.h", + "line": 16 + }, + { + "path": "src/physics/cartire1.cpp", + "line": 84 + }, + { + "path": "src/physics/cartire1.cpp", + "line": 302 + } + ], + "capabilities": [ + "camber thrust", + "horizontal shift", + "series approximation", + "turn-in feel" + ], + "tags": [ + "physics", + "tyres", + "handling", + "feel" + ] + }, + { + "id": "engine-torque-and-friction", + "node": "drivetrain", + "name": "Engine torque, friction and rev limiting", + "summary": "A tabulated torque curve scaled by throttle, minus a quadratic friction torque, with idle throttle solved at load and a hard rev limiter", + "behavior": "The engine is a shaft driven by two opposing torques. Combustion torque comes from a tabulated curve of flywheel torque against engine speed, scaled linearly by throttle, with the curve extended down to the stall speed and up to the rev limit by linear extrapolation from the outermost fitted points. Friction torque grows with engine speed as a quadratic, its three coefficients derived from displacement unless the car supplies its own, and is scaled by how far the throttle is closed. At load time the model also searches for the smallest throttle that keeps the engine turning at its start speed and derives a slope so idle throttle rises as speed drops and the engine will not stall at rest. Above the rev limit combustion is cut and the limit is dropped by a hundred revolutions so the engine does not chatter on the limiter, and a stalled engine gets ten times its friction.", + "constants": [ + { + "name": "defaultRedline", + "value": 7800, + "unit": "revolutions per minute, a static setup value read every 1/90 s tick", + "source": "src/physics/carengine.cpp:28" + }, + { + "name": "defaultRevLimit", + "value": 9000, + "unit": "revolutions per minute before combustion is cut, checked every 1/90 s tick", + "source": "src/physics/carengine.cpp:29" + }, + { + "name": "defaultStallSpeed", + "value": 350, + "unit": "revolutions per minute below which the engine is treated as stalled, checked every 1/90 s tick", + "source": "src/physics/carengine.cpp:33" + }, + { + "name": "frictionLinearCoefficient", + "value": 15000, + "unit": "friction torque coefficient for the linear speed term, scaled by displacement at load and read every 1/90 s tick", + "source": "src/physics/carengine.cpp:74" + }, + { + "name": "revLimitHysteresis", + "value": 100, + "unit": "revolutions per minute of drop while the limiter is active, each 1/90 s tick", + "source": "src/physics/carengine.cpp:204" + }, + { + "name": "stalledFrictionMultiplier", + "value": 10, + "unit": "multiplier on friction torque while stalled, applied every 1/90 s tick", + "source": "src/physics/carengine.cpp:228" + } + ], + "sources": [ + { + "path": "src/physics/carengine.cpp", + "line": 45 + }, + { + "path": "src/physics/carengine.cpp", + "line": 73 + }, + { + "path": "src/physics/carengine.cpp", + "line": 156 + }, + { + "path": "src/physics/carengine.cpp", + "line": 166 + }, + { + "path": "src/physics/carengine.cpp", + "line": 191 + } + ], + "capabilities": [ + "torque curve", + "friction torque", + "idle control", + "rev limiter", + "engine stall" + ], + "tags": [ + "drivetrain", + "engine", + "feel", + "sound" + ] + }, + { + "id": "clutch-and-auto-shift", + "node": "drivetrain", + "name": "Clutch capacity and automatic shifting", + "summary": "Clutch torque capacity comes from pressure, area and mean radius; an optional automatic clutch and shifter rate-limit engagement and pick gears from wheel speed", + "behavior": "Clutch capacity is computed once from plate pressure times friction area times the friction coefficient times the mean effective radius, and the transmitted torque is that capacity multiplied by pedal position, so a partially released clutch slips and a fully released one locks the engine to the gearbox. With the automatic clutch enabled, engagement is a control problem: above the start speed the clutch closes, and below it the closing is limited by how far the engine is above a floor that sits midway between stall and start speed, with the throttle leaning that floor. Engagement and disengagement are rate-limited asymmetrically, and mid-shift the clutch is opened and the throttle is either held or halved to smooth the torque cut. The automatic shifter watches clutch-side speed, selects the next gear when it passes the redline and drops a gear when it falls below a fraction of the redline scaled by the ratio gap.", + "constants": [ + { + "name": "clutchFrictionCoefficient", + "value": 0.4, + "unit": "unitless plate friction used in the capacity product, a static setup value, no per-tick rate", + "source": "src/physics/carclutch.h:29" + }, + { + "name": "clutchPlatePressure", + "value": 250000, + "unit": "pascals of clamping pressure, a static setup value, no per-tick rate", + "source": "src/physics/carclutch.h:30" + }, + { + "name": "clutchMeanRadius", + "value": 0.09, + "unit": "metres, the mean effective friction radius, a static setup value, no per-tick rate", + "source": "src/physics/carclutch.h:31" + }, + { + "name": "clutchFrictionArea", + "value": 0.04, + "unit": "square metres of friction surface, a static setup value, no per-tick rate", + "source": "src/physics/carclutch.h:32" + }, + { + "name": "defaultShiftTime", + "value": 0.2, + "unit": "seconds, counted down once per 1/90 s tick while a shift is in progress", + "source": "src/physics/cartransmission.h:36" + }, + { + "name": "autoClutchEngageRate", + "value": 10, + "unit": "fraction of full clutch travel per second, so full engagement takes about 0.1 s at 1/90 s ticks", + "source": "src/physics/cardynamics.cpp:1581" + }, + { + "name": "downshiftSpeedFraction", + "value": 0.7, + "unit": "fraction of redline in the lower gear before an automatic downshift, checked every 1/90 s tick", + "source": "src/physics/cardynamics.cpp:1661" + }, + { + "name": "shiftThrottleCut", + "value": 0.5, + "unit": "fraction of throttle retained during a shift, applied every 1/90 s tick", + "source": "src/physics/cardynamics.cpp:1624" + } + ], + "sources": [ + { + "path": "src/physics/carclutch.h", + "line": 38 + }, + { + "path": "src/physics/cartransmission.h", + "line": 106 + }, + { + "path": "src/physics/cardynamics.cpp", + "line": 1579 + }, + { + "path": "src/physics/cardynamics.cpp", + "line": 1630 + }, + { + "path": "src/physics/cardynamics.cpp", + "line": 1772 + }, + { + "path": "src/physics/cardynamics.cpp", + "line": 55 + } + ], + "capabilities": [ + "clutch capacity", + "clutch slip", + "automatic clutch", + "automatic gearbox", + "torque cut" + ], + "tags": [ + "drivetrain", + "clutch", + "gearbox", + "aids" + ] + }, + { + "id": "differential-and-torque-split", + "node": "drivetrain", + "name": "Limited-slip differential and drive layout", + "summary": "Final drive plus a speed-sensitive anti-slip coupling and an adjustable centre split decide how torque reaches the wheels", + "behavior": "Each differential carries a final drive ratio, an anti-slip torque, an optional torque-sensing term with a one-way or two-way factor, and a split value used by centre differentials. The driveline is assembled as a small network of shafts: the engine shaft, one shaft per driven wheel, and a coupling per differential whose softness is derived from the anti-slip torque and the substep so that the coupling reaches its maximum torque at a defined slip speed. Because the anti-slip behaviour is expressed as a coupling softness, a load coefficient and a per-substep impulse limit, a welded spool, a clutch-type limited slip and a viscous unit are all described by different numbers on the same coupling. Front-wheel, rear-wheel and all-wheel layouts select which differential feeds which shafts, and the all-wheel path multiplies a centre ratio into the total, so gearing feels different on each layout.", + "constants": [ + { + "name": "defaultFinalDrive", + "value": 4.1, + "unit": "unitless wheel revs per driveshaft rev, a static setup value, no per-tick rate", + "source": "src/physics/cardifferential.h:30" + }, + { + "name": "defaultAntiSlipTorque", + "value": 600, + "unit": "newton metres of locking torque before slip, applied every 1/900 s substep", + "source": "src/physics/cardifferential.h:31" + }, + { + "name": "defaultTorqueSplit", + "value": 0.5, + "unit": "unitless share of centre torque to the first side, a static setup value, no per-tick rate", + "source": "src/physics/cardifferential.h:34" + }, + { + "name": "antiSlipReferenceSlipSpeed", + "value": 12, + "unit": "radians per second of coupling slip at which anti-slip torque peaks, each 1/900 s substep", + "source": "src/physics/cardynamics.cpp:1274" + } + ], + "sources": [ + { + "path": "src/physics/cardifferential.h", + "line": 28 + }, + { + "path": "src/physics/cardynamics.cpp", + "line": 1246 + }, + { + "path": "src/physics/cardynamics.cpp", + "line": 1262 + }, + { + "path": "src/physics/cardynamics.cpp", + "line": 1285 + } + ], + "capabilities": [ + "limited slip", + "torque sensing", + "centre split", + "drive layout", + "final drive" + ], + "tags": [ + "drivetrain", + "differential", + "handling", + "setup" + ] + }, + { + "id": "abs-brake-modulation", + "node": "car-dynamics", + "name": "Brake torque and ABS modulation", + "summary": "Brake torque scales with pedal, bias and handbrake; ABS limits how fast brake pressure may rise relative to predicted slip", + "behavior": "Brake capacity per wheel is pressure times pad area times pad friction times rotor radius, and applied torque is that capacity scaled by pedal travel times the bias plus the handbrake input times its own bias, with the handbrake deliberately kept on a separate factor so that the anti-lock loop cannot clamp it. With anti-lock enabled and the car moving above roughly four metres per second, the model extrapolates next tick slip from the change in slip over the last tick, compares it with the peak-slip table, and allows brake pressure to rise only by the remaining headroom, so pressure is released before the wheel actually locks. Below that speed, or with the aid off, the pedal value passes straight through. The result is a brake pedal that can be buried without spinning the car, and a handbrake that still locks the rear axle on demand.", + "constants": [ + { + "name": "brakePadFriction", + "value": 0.73, + "unit": "unitless pad-on-rotor friction, a static setup value, no per-tick rate", + "source": "src/physics/carbrake.h:30" + }, + { + "name": "brakeRotorRadius", + "value": 0.14, + "unit": "metres of effective rotor radius, a static setup value, no per-tick rate", + "source": "src/physics/carbrake.h:32" + }, + { + "name": "brakePadArea", + "value": 0.015, + "unit": "square metres of pad area, a static setup value, no per-tick rate", + "source": "src/physics/carbrake.h:33" + }, + { + "name": "absMinimumSquaredSpeed", + "value": 16, + "unit": "square metres per second squared, so anti-lock engages above 4 m/s, checked every 1/90 s tick", + "source": "src/physics/cardynamics.cpp:1128" + }, + { + "name": "absPressureRiseGain", + "value": 2, + "unit": "multiplier on the remaining slip headroom per tick, evaluated every 1/90 s tick", + "source": "src/physics/cardynamics.cpp:1135" + } + ], + "sources": [ + { + "path": "src/physics/carbrake.h", + "line": 43 + }, + { + "path": "src/physics/carbrake.h", + "line": 78 + }, + { + "path": "src/physics/cardynamics.cpp", + "line": 1124 + }, + { + "path": "src/physics/cardynamics.cpp", + "line": 1131 + }, + { + "path": "src/physics/cardynamics.cpp", + "line": 1148 + } + ], + "capabilities": [ + "brake bias", + "handbrake", + "anti-lock braking", + "slip prediction", + "pressure limiting" + ], + "tags": [ + "brakes", + "aids", + "physics", + "feel" + ] + }, + { + "id": "traction-control", + "node": "car-dynamics", + "name": "Traction control", + "summary": "A throttle-side aid with hysteresis: it cuts throttle once slip passes the peak value by a margin and restores it only after slip falls well below", + "behavior": "Traction control works on the same peak-slip reference as the anti-lock loop but acts on the throttle rather than the brakes. It stays inactive until the driver asks for more than a tenth of throttle, then computes a target slip from the peak value tapered by the cosine of the current slip angle so it tightens up as the car slides sideways. The aid engages when measured slip exceeds the target by twenty percent and disengages only when slip falls below eighty percent of it, which prevents the on-off chatter a single threshold would produce. While engaged, commanded throttle is reduced in proportion to the excess slip scaled by clutch position and then clamped, so the car keeps the driver command as an upper bound instead of a fixed value. The feel is a rear end that steps out briefly and then hooks up.", + "constants": [ + { + "name": "throttleActivationThreshold", + "value": 0.1, + "unit": "fraction of full throttle, checked every 1/90 s tick", + "source": "src/physics/cardynamics.cpp:1098" + }, + { + "name": "slipEngageFactor", + "value": 1.2, + "unit": "multiple of the peak slip value at which traction control cuts, each 1/90 s tick", + "source": "src/physics/cardynamics.cpp:1104" + }, + { + "name": "slipDisengageFactor", + "value": 0.8, + "unit": "multiple of the peak slip value at which control releases, each 1/90 s tick", + "source": "src/physics/cardynamics.cpp:1105" + }, + { + "name": "throttleCorrectionGain", + "value": 2, + "unit": "multiplier on excess slip per tick, applied every 1/90 s tick", + "source": "src/physics/cardynamics.cpp:1115" + } + ], + "sources": [ + { + "path": "src/physics/cardynamics.cpp", + "line": 1094 + }, + { + "path": "src/physics/cardynamics.cpp", + "line": 1102 + }, + { + "path": "src/physics/cardynamics.cpp", + "line": 1107 + }, + { + "path": "src/physics/cardynamics.cpp", + "line": 1116 + } + ], + "capabilities": [ + "traction control", + "slip hysteresis", + "throttle intervention", + "slip-angle taper" + ], + "tags": [ + "aids", + "physics", + "drivetrain", + "feel" + ] + }, + { + "id": "surface-grip-and-bumps", + "node": "wheel-constraint", + "name": "Surface friction, drag and road bumps", + "summary": "Each track surface carries separate tread and sidewall friction, a rolling resistance coefficient, a rolling drag and a sinusoidal bump field", + "behavior": "Surfaces are named types, each carrying a friction figure for the treaded part of the tyre and a separate one for the sidewall, so a car that slides on its shoulders loses less grip than one that slides flat. The per-wheel friction actually used is a linear blend between those two by the tyre tread fraction. Alongside friction each surface supplies a rolling resistance coefficient that scales a speed-proportional and a speed-squared resistance term, a rolling drag used as a velocity-proportional impulse that is clipped so it can never reverse a car, and a bump field described by a wavelength and an amplitude. Bump height is sampled as a smoothed triangular wave of the contact position in two horizontal axes, multiplied by amplitude and halved, then added to the suspension displacement. Switching from asphalt to grass therefore changes grip, drag and ride quality at once.", + "constants": [ + { + "name": "surfaceAsphalt", + "value": "asphalt", + "unit": "surface type name matched once when track geometry is loaded, no per-tick rate", + "source": "src/physics/tracksurface.h:42" + }, + { + "name": "defaultBumpWavelength", + "value": 1, + "unit": "metres per bump cycle, sampled every 1/90 s tick", + "source": "src/physics/tracksurface.h:59" + }, + { + "name": "bumpAmplitudeShare", + "value": 0.5, + "unit": "fraction of surface bump amplitude added to travel, once per 1/90 s tick", + "source": "src/physics/cardynamics.cpp:1175" + } + ], + "sources": [ + { + "path": "src/physics/tracksurface.h", + "line": 74 + }, + { + "path": "src/physics/tracksurface.h", + "line": 70 + }, + { + "path": "src/physics/cardynamics.cpp", + "line": 1370 + }, + { + "path": "src/physics/cardynamics.cpp", + "line": 1392 + }, + { + "path": "src/physics/cardynamics.cpp", + "line": 1167 + } + ], + "capabilities": [ + "surface types", + "tread blend", + "rolling resistance", + "rolling drag", + "bump field" + ], + "tags": [ + "physics", + "surfaces", + "off-road", + "feel" + ] + }, + { + "id": "track-patch-raycast", + "node": "track-geometry", + "name": "Ray against track patches", + "summary": "A ray is intersected with the nearest bounding-volume candidate patch by recursive parameter-quad subdivision, returning the hit patch for sector and surface lookup", + "behavior": "Track roads are stored as chains of four-by-four control-point surface patches, each linked to its successor and gathered into a bounding-volume index. A query first tests the patch the car was on last tick as a fast path, then asks the index for candidates near the ray. Each candidate is tested by repeatedly intersecting the ray with the flat quad spanned by the current parameter window and, on a hit, shrinking the window around the hit and testing again, so a curved patch is resolved to a small parameter region without tessellation. The hit with the smallest distance wins and the owning patch pointer travels back with the hit, which is what lets the game name a sector, look up a surface and tell the AI which piece of road it is on. Strip closure is detected by comparing endpoint corners within a small distance.", + "constants": [ + { + "name": "subdivisionDepth", + "value": 6, + "unit": "parameter-quad subdivision levels per patch, per ray, once per 1/90 s tick", + "source": "src/bezier.cpp:359" + }, + { + "name": "subdivisionAreaCut", + "value": 0.5, + "unit": "fraction of the parameter window retained around a hit each level, per subdivision", + "source": "src/bezier.cpp:360" + }, + { + "name": "stripClosureTolerance", + "value": 0.01, + "unit": "square metres of squared corner distance for a strip to count as closed, checked at load", + "source": "src/roadstrip.cpp:78" + } + ], + "sources": [ + { + "path": "src/track.cpp", + "line": 128 + }, + { + "path": "src/roadstrip.cpp", + "line": 106 + }, + { + "path": "src/roadstrip.cpp", + "line": 77 + }, + { + "path": "src/roadpatch.cpp", + "line": 91 + }, + { + "path": "src/bezier.cpp", + "line": 356 + } + ], + "capabilities": [ + "bezier patches", + "bounding volume index", + "parameter subdivision", + "nearest-hit resolution", + "closed circuits" + ], + "tags": [ + "track", + "collision", + "geometry", + "query" + ] + }, + { + "id": "ai-speed-planning", + "node": "ai-drivers", + "name": "Corner speed limit and braking lookahead", + "summary": "Corner speed comes from patch curvature and a lateral friction factor; a braking-distance walk along the strip decides when to lift", + "behavior": "An opponent first reads the patch under its front wheels and trims it toward the racing line. It then asks for the speed that curvature and friction allow: the corner radius, widened by the road width when the next patch opens out, is fed into a maximum-speed solve that also accounts for aerodynamic lift changing the normal load, scaled by a lateral friction factor below the tyre peak so the driver has margin. The result is multiplied by a per-driver difficulty value. Locally, the difference between that limit and the car heading speed becomes throttle or brake through a proportional band with a dead zone, and both inputs are then rate-limited so the driver cannot stamp on the pedals. For corners further ahead, the driver walks forward along the strip for a distance equal to its own braking distance plus a margin and brakes fully as soon as a slower corner is closer than the distance needed to slow for it.", + "constants": [ + { + "name": "speedBand", + "value": 6, + "unit": "metres per second of speed error mapped to full throttle, evaluated every 1/90 s tick", + "source": "src/ai/ai_car_standard.cpp:34" + }, + { + "name": "speedDeadBand", + "value": 1, + "unit": "metres per second of overspeed tolerated before braking, evaluated every 1/90 s tick", + "source": "src/ai/ai_car_standard.cpp:35" + }, + { + "name": "longitudinalFrictionFactor", + "value": 0.68, + "unit": "fraction of tyre peak used for AI braking distance, evaluated every 1/90 s tick", + "source": "src/ai/ai_car_standard.cpp:45" + }, + { + "name": "lateralFrictionFactor", + "value": 0.62, + "unit": "fraction of tyre peak used for AI corner speed, evaluated every 1/90 s tick", + "source": "src/ai/ai_car_standard.cpp:46" + }, + { + "name": "pedalRateLimit", + "value": 0.1, + "unit": "fraction of full pedal travel per second, applied every 1/90 s tick", + "source": "src/ai/ai_car_standard.cpp:49" + }, + { + "name": "brakingLookaheadMargin", + "value": 10, + "unit": "metres added to the braking distance searched ahead, evaluated every 1/90 s tick", + "source": "src/ai/ai_car_standard.cpp:336" + } + ], + "sources": [ + { + "path": "src/ai/ai_car_standard.cpp", + "line": 267 + }, + { + "path": "src/ai/ai_car_standard.cpp", + "line": 393 + }, + { + "path": "src/ai/ai_car_standard.cpp", + "line": 336 + }, + { + "path": "src/ai/ai_car_standard.cpp", + "line": 377 + }, + { + "path": "src/physics/cardynamics.cpp", + "line": 1006 + } + ], + "capabilities": [ + "corner speed limit", + "friction margin", + "braking distance", + "pedal rate limiting", + "difficulty scaling" + ], + "tags": [ + "ai", + "driving", + "physics", + "racing" + ] + }, + { + "id": "ai-steering-and-traffic", + "node": "ai-drivers", + "name": "AI steering lookahead and traffic analysis", + "summary": "Steering aims at a lookahead point accumulated patch by patch, while every other car is tracked in the driver frame for braking and overtaking", + "behavior": "Steering is pursuit along the road strip. The driver starts at the next patch and keeps advancing while the accumulated centreline distance is under one metre, adding twice each patch length per step, so it ends up aiming several car lengths ahead; the walk also stops early if the upcoming patch curves tighter than a minimum radius, which keeps the driver from cutting a hairpin. The destination is the centre of the front edge of the final patch. The angle between the car heading and the direction to that point, both flattened to the horizontal plane, is divided by the car maximum steering lock and clamped, and that becomes the steering command. Alongside steering, every tick projects every other car into the driver frame and records its fore distance, lateral offset, closing speed and an estimated time to contact, which is the data a driver would need to brake for a slow car or steer around one. The two bias helpers that consume that data express sensible intent: one builds a braking bias that multiplies ramps over reaction time, separation and closing speed, the other builds a lateral bias that saturates at a fixed spacing, but in this revision the hooks that would add those biases to the pedals and the steering angle are commented out, so only the analysis runs each tick.", + "constants": [ + { + "name": "lookaheadPatchStride", + "value": 2, + "unit": "patch-length multiplier per lookahead step, recomputed every 1/90 s tick", + "source": "src/ai/ai_car_standard.cpp:454" + }, + { + "name": "lookaheadStartDistance", + "value": 1, + "unit": "metres of centreline distance before the lookahead point is accepted, each 1/90 s tick", + "source": "src/ai/ai_car_standard.cpp:444" + }, + { + "name": "sharpCornerRadius", + "value": 8, + "unit": "metres of patch radius that aborts the lookahead walk, checked every 1/90 s tick", + "source": "src/ai/ai_car_standard.cpp:42" + }, + { + "name": "steeringCommandClamp", + "value": 1, + "unit": "fraction of maximum steering lock, clamped every 1/90 s tick", + "source": "src/ai/ai_car_standard.cpp:506" + }, + { + "name": "trafficLateralCare", + "value": 2.5, + "unit": "metres either side of the driver that count as in the way, evaluated every 1/90 s tick", + "source": "src/ai/ai_car_standard.cpp:529" + }, + { + "name": "trafficBrakeStartDistance", + "value": 10, + "unit": "metres of separation at which a braking bias would start, evaluated every 1/90 s tick", + "source": "src/ai/ai_car_standard.cpp:532" + }, + { + "name": "avoidanceSpacing", + "value": 3.5, + "unit": "metres of lateral offset targeted when steering around another car, each 1/90 s tick", + "source": "src/ai/ai_car_standard.cpp:645" + } + ], + "sources": [ + { + "path": "src/ai/ai_car_standard.cpp", + "line": 413 + }, + { + "path": "src/ai/ai_car_standard.cpp", + "line": 448 + }, + { + "path": "src/ai/ai_car_standard.cpp", + "line": 474 + }, + { + "path": "src/ai/ai_car_standard.cpp", + "line": 495 + }, + { + "path": "src/ai/ai_car_standard.cpp", + "line": 578 + }, + { + "path": "src/ai/ai_car_standard.cpp", + "line": 643 + } + ], + "capabilities": [ + "pure pursuit steering", + "lookahead point", + "hairpin protection", + "relative car tracking", + "unused avoidance biases" + ], + "tags": [ + "ai", + "steering", + "traffic", + "racing" + ] + }, + { + "id": "racing-line-generation", + "node": "racing-line", + "name": "Generated racing line", + "summary": "A coarse-to-fine relaxation pass slides a lateral lane value at every road patch until curvature is minimised, respecting edge margins", + "behavior": "Each road patch in a strip becomes one node, with its left and right edge points taken from the patch corners and its lane value seeded at the middle of the road. The optimiser then works from coarse to fine: it starts with a large step and halves it down to one, and at each resolution it runs relaxation passes proportional to the square root of the step, moving every step-th node, then interpolates the nodes in between by matching a target curvature. A relaxation measures the inverse radius of the circle through the node and its two neighbours, nudges the lane value by a small Newton-like step toward a slightly larger radius, and then clamps the result against margins measured in from each edge and against the road width. Because the target curvature at each node is a length-weighted blend of the curvature measured at its two neighbours, the path converges to a smooth line that straightens corner entries rather than hugging the inside. The finished path and its per-node curvature are written back onto the strip for the drivers and the drawn racing line.", + "constants": [ + { + "name": "securityRadius", + "value": 100, + "unit": "metres of reference radius that scales the safety slack added to the edge margins, applied once per relaxation", + "source": "src/k1999.cpp:34" + }, + { + "name": "outsideMargin", + "value": 2, + "unit": "metres of margin kept from the outer edge, applied once per relaxation", + "source": "src/k1999.cpp:35" + }, + { + "name": "insideMargin", + "value": 1, + "unit": "metres of margin kept from the inner edge, applied once per relaxation", + "source": "src/k1999.cpp:36" + }, + { + "name": "relaxationsPerSqrtStep", + "value": 100, + "unit": "relaxation passes per resolution level, multiplied by the square root of the step size, run once at track load", + "source": "src/k1999.cpp:37" + }, + { + "name": "initialSmoothingStep", + "value": 128, + "unit": "patches in the first, coarsest smoothing step, halved each level at track load", + "source": "src/k1999.cpp:248" + }, + { + "name": "initialLaneValue", + "value": 0.5, + "unit": "fraction from the left edge at seeding, written once per patch at track load", + "source": "src/k1999.cpp:317" + } + ], + "sources": [ + { + "path": "src/k1999.cpp", + "line": 246 + }, + { + "path": "src/k1999.cpp", + "line": 257 + }, + { + "path": "src/k1999.cpp", + "line": 287 + }, + { + "path": "src/k1999.cpp", + "line": 327 + }, + { + "path": "src/k1999.cpp", + "line": 174 + } + ], + "capabilities": [ + "racing line", + "curvature minimisation", + "coarse-to-fine relaxation", + "edge margins", + "strip write-back" + ], + "tags": [ + "ai", + "track", + "racing line", + "preprocessing" + ] + }, + { + "id": "lap-and-sector-timing", + "node": "race-timing", + "name": "Lap and sector timing", + "summary": "A lap counts when a wheel touches the next sector patch and the sector index wraps to zero; staging time is excluded from the clock", + "behavior": "Each car owns a running lap clock, a last and best lap, a sector index and an accumulated lap distance. Every tick the game computes the expected next sector as the current sector plus one modulo the sector count, then checks all four wheel contacts against that sector patch; a match advances the car to that sector. The car only records a completed lap when the advanced sector wraps back to zero, which is what stops a car parked on the finish patch from banking laps, and sector times are also written back as per-car-type records. The clock subtracts staging time while the countdown is running so the formation period is not charged to the lap. Lap distance is tracked separately for placement, which orders cars by completed laps and then by distance.", + "constants": [ + { + "name": "stagingCountdown", + "value": 3, + "unit": "seconds of counted-down staging removed from lap clocks, ticked down each 1/90 s tick", + "source": "src/game.cpp:1639" + }, + { + "name": "sectorWrapIndex", + "value": 0, + "unit": "sector index that closes a lap, compared every 1/90 s tick", + "source": "src/timer.cpp:115" + }, + { + "name": "wheelsProbedPerLapCheck", + "value": 4, + "unit": "wheel contacts tested against the sector patch, once per 1/90 s tick", + "source": "src/game.cpp:995" + } + ], + "sources": [ + { + "path": "src/timer.cpp", + "line": 68 + }, + { + "path": "src/timer.cpp", + "line": 90 + }, + { + "path": "src/timer.cpp", + "line": 119 + }, + { + "path": "src/game.cpp", + "line": 984 + }, + { + "path": "src/game.cpp", + "line": 1005 + } + ], + "capabilities": [ + "lap clock", + "sector detection", + "staging countdown", + "best lap records", + "placement ordering" + ], + "tags": [ + "race", + "timing", + "hud", + "rules" + ] + }, + { + "id": "drift-scoring", + "node": "game-loop", + "name": "Drift scoring", + "summary": "Drift is the angle between heading and velocity above a speed floor, scored by distance travelled with speed and angle bonuses", + "behavior": "The scorer first requires the car to be on the track, meaning more than one wheel has a track patch, and moving faster than ten metres per second. It then flattens both heading and velocity to the horizontal plane and takes the angle between them. Drifting begins when that angle passes about eleven degrees and, once drifting, continues until it falls below about six degrees, which gives the state a wide hysteresis band so a wobble does not break a chain. Anything past a right angle is a spin and voids the drift. The base score is distance travelled while drifting, and the banked total adds a bonus of half the maximum speed reached plus about twelve times the maximum angle in radians, but only if the accumulated drift is worth more than a token amount, so a brief twitch scores nothing.", + "constants": [ + { + "name": "driftSpeedFloor", + "value": 10, + "unit": "metres per second, checked every 1/90 s tick", + "source": "src/game.cpp:2624" + }, + { + "name": "driftEnterAngle", + "value": 0.2, + "unit": "radians between heading and velocity, evaluated every 1/90 s tick", + "source": "src/game.cpp:2634" + }, + { + "name": "driftExitAngle", + "value": 0.1, + "unit": "radians, the relaxed threshold used once drifting, evaluated every 1/90 s tick", + "source": "src/game.cpp:2635" + }, + { + "name": "bonusSpeedWeight", + "value": 0.5, + "unit": "score per metre per second of peak drift speed, awarded once per banked drift", + "source": "src/timer.h:212" + }, + { + "name": "bonusAngleWeight", + "value": 40, + "unit": "score numerator per pi radians of peak drift angle, awarded once per banked drift", + "source": "src/timer.h:212" + }, + { + "name": "bankThreshold", + "value": 5, + "unit": "score points a drift must reach to be banked, checked once per 1/90 s tick", + "source": "src/timer.h:158" + } + ], + "sources": [ + { + "path": "src/game.cpp", + "line": 2595 + }, + { + "path": "src/game.cpp", + "line": 2608 + }, + { + "path": "src/game.cpp", + "line": 2637 + }, + { + "path": "src/timer.h", + "line": 210 + } + ], + "capabilities": [ + "drift detection", + "hysteresis thresholds", + "distance scoring", + "speed and angle bonus", + "spin penalty" + ], + "tags": [ + "race", + "scoring", + "drift", + "hud" + ] + }, + { + "id": "chase-and-orbit-cameras", + "node": "camera", + "name": "Chase and orbit cameras", + "summary": "A chase rig carries an offset in car space and blends toward it; an orbit rig adds clamped yaw, pitch and distance", + "behavior": "The chase camera keeps a fixed offset in the car frame, two metres up and six metres behind, rotates that offset by the car orientation every frame and then moves toward the resulting target position with a blend proportional to frame time, capped so a long frame cannot snap it. Orientation is a look-at from the new position to the car, so the car is always centred and the camera lags into corners rather than rigidly following. The offset is validated so it can never sit in front of the car. The orbit camera is the photo rig: it holds a distance derived from the offset length, accumulates yaw and pitch from mouse or pad input with pitch clamped to about a radian and a half, and lets the user zoom the distance between one and four times the resting standoff. Both rigs orbit the same focus point, but only the chase rig derives its orientation by looking at the car; the orbit rig instead rebuilds its rotation from the accumulated yaw and pitch and places itself at the clamped distance along the rotated view axis.", + "constants": [ + { + "name": "chaseHeight", + "value": 2, + "unit": "metres above the car in car-local space, applied every frame", + "source": "src/camera_chase.cpp:24" + }, + { + "name": "chaseTrail", + "value": 6, + "unit": "metres behind the car in car-local space, applied every frame", + "source": "src/camera_chase.cpp:24" + }, + { + "name": "positionBlendRate", + "value": 10, + "unit": "per second of frame time, saturating to a snap, applied every rendered frame", + "source": "src/camera_chase.cpp:55" + }, + { + "name": "orbitRestingStandoff", + "value": -1.5, + "unit": "multiples of the offset length along the forward axis, set when the orbit rig resets, no per-tick rate", + "source": "src/camera_orbit.cpp:26" + }, + { + "name": "orbitPitchClamp", + "value": 1.5, + "unit": "radians of accumulated pitch either way, applied every frame the rig rotates", + "source": "src/camera_orbit.cpp:59" + } + ], + "sources": [ + { + "path": "src/camera_chase.cpp", + "line": 48 + }, + { + "path": "src/camera_chase.cpp", + "line": 59 + }, + { + "path": "src/camera_orbit.cpp", + "line": 57 + }, + { + "path": "src/camera_orbit.cpp", + "line": 68 + } + ], + "capabilities": [ + "chase camera", + "position blending", + "look-at orientation", + "orbit camera", + "zoom clamping" + ], + "tags": [ + "camera", + "follow", + "smoothing", + "view" + ] + }, + { + "id": "input-shaping", + "node": "controls", + "name": "Analogue input shaping", + "summary": "Dead zone, gain, exponent and a signed button-to-axis ramp sit between the device and the car", + "behavior": "Raw device values are shaped before the car ever sees them. An axis first has its sign flipped if the binding is inverted, then a dead zone is removed by subtracting the threshold and rescaling the remainder back to full travel, then gain is applied and clipped to unit magnitude, and finally a response exponent is applied to the magnitude with the original sign restored, which lets a pad produce fine inputs near centre and still reach full lock. Buttons converted to axes use a ramp instead: the value moves toward the target at a configured rate rather than jumping, and the rate is deliberately asymmetric, climbing at roughly a quarter of the speed it falls, so releasing a key gives a faster return to neutral than pressing it. This is what makes keyboard driving in a physics-driven car usable at all.", + "constants": [ + { + "name": "rampRiseScale", + "value": 0.3, + "unit": "fraction of the configured ramp rate applied while pressing, per second of input time at 90 Hz", + "source": "src/carcontrolmap.cpp:45" + }, + { + "name": "rampFallScale", + "value": -1.2, + "unit": "fraction of the configured ramp rate applied while releasing (signed so the value falls), per second of input time at 90 Hz", + "source": "src/carcontrolmap.cpp:47" + }, + { + "name": "gainClamp", + "value": 1, + "unit": "magnitude ceiling after gain, applied every shaped axis read at 90 Hz", + "source": "src/carcontrolmap.cpp:65" + } + ], + "sources": [ + { + "path": "src/carcontrolmap.cpp", + "line": 34 + }, + { + "path": "src/carcontrolmap.cpp", + "line": 55 + }, + { + "path": "src/carcontrolmap.cpp", + "line": 68 + }, + { + "path": "src/carcontrolmap.cpp", + "line": 550 + } + ], + "capabilities": [ + "dead zone", + "axis gain", + "response exponent", + "button ramp", + "asymmetric release" + ], + "tags": [ + "input", + "controls", + "feel", + "accessibility" + ] + }, + { + "id": "aero-downforce-and-drag", + "node": "car-dynamics", + "name": "Aerodynamic downforce and drag", + "summary": "Wings and bodywork add speed-squared lift and drag, with induced drag from the wing and reduced lift when travelling backwards", + "behavior": "Each aerodynamic device converts its setup numbers into coefficients once: a lift coefficient from half air density times the wing coefficient times wing area, an induced drag coefficient from that lift and the wing efficiency, and a drag coefficient from half density times the body coefficient times frontal area. Per tick, drag is the coefficient times wind speed times the wind vector, and lift is the coefficient times the square of the forward wind speed, pushed along the body up axis with the induced drag component pushed backwards. Wind speed is taken as the negative forward component, and if the car is travelling backwards the lift is scaled down sharply, so reversing produces little downforce. The downforce is applied at the wing position rather than the centre of mass, so a strong wing also changes the pitch balance, and it raises the load the tyres see, which is why top speed and corner speed trade directly.", + "constants": [ + { + "name": "airDensity", + "value": 1.2, + "unit": "kilograms per cubic metre, a static value folded into the coefficients at load", + "source": "src/physics/aerodevice.h:38" + }, + { + "name": "dynamicPressureFactor", + "value": 0.5, + "unit": "half, the dynamic-pressure factor applied once when coefficients are built at load", + "source": "src/physics/aerodevice.h:118" + }, + { + "name": "reverseLiftFactor", + "value": 0.2, + "unit": "fraction of lift retained when travelling backwards, applied every 1/90 s tick", + "source": "src/physics/aerodevice.h:88" + } + ], + "sources": [ + { + "path": "src/physics/aerodevice.h", + "line": 74 + }, + { + "path": "src/physics/aerodevice.h", + "line": 118 + }, + { + "path": "src/physics/cardynamics.cpp", + "line": 1076 + }, + { + "path": "src/physics/cardynamics.cpp", + "line": 1010 + } + ], + "capabilities": [ + "downforce", + "induced drag", + "wing balance", + "reverse lift cut", + "top speed trade-off" + ], + "tags": [ + "physics", + "aero", + "handling", + "setup" + ] + }, + { + "id": "suspension-bump-events", + "node": "suspension", + "name": "Suspension bump and landing events", + "summary": "A four-state machine turns suspension velocity and displacement into discrete bottoming hints for sound and effects", + "behavior": "Physics produces continuous travel, but sound and effects want discrete events. A small state machine watches suspension velocity and displacement: when velocity climbs past a threshold the state moves to displacing and starts a timer; if velocity stays high for the full timer window the state moves to displaced, and when velocity falls back the state moves to settling and holds until a second timer expires, then settles. Each transition is exposed as an edge the presentation layer can react to, along with start and end displacement, whose difference is the total size of the hit. Because both the velocity threshold and the timer lengths are explicit, a slow compression over a kerb and a hard landing off a raised patch look different to the audio and particle systems even though both end at full travel.", + "constants": [ + { + "name": "displaceHoldTime", + "value": 0.01, + "unit": "seconds of sustained compression before a bump is reported, ticked down every 1/90 s tick", + "source": "src/suspensionbumpdetection.cpp:25" + }, + { + "name": "displaceVelocityThreshold", + "value": 0.5, + "unit": "suspension travel per second, checked every 1/90 s tick", + "source": "src/suspensionbumpdetection.cpp:26" + }, + { + "name": "settleHoldTime", + "value": 0.01, + "unit": "seconds of settled travel before the event closes, ticked down every 1/90 s tick", + "source": "src/suspensionbumpdetection.cpp:27" + } + ], + "sources": [ + { + "path": "src/suspensionbumpdetection.cpp", + "line": 37 + }, + { + "path": "src/suspensionbumpdetection.h", + "line": 30 + }, + { + "path": "src/suspensionbumpdetection.h", + "line": 42 + } + ], + "capabilities": [ + "bump detection", + "landing detection", + "event edges", + "effect triggering" + ], + "tags": [ + "physics", + "suspension", + "effects", + "audio" + ] + }, + { + "id": "skid-mark-emission", + "node": "car-visuals", + "name": "Skid marks and tyre squeal", + "summary": "Per-wheel squeal intensity from normalised slip feeds a decaying emitter that only lays a mark once enough energy accumulates", + "behavior": "Squeal is computed per wheel from the normalised slip ratio and slip angle: each is divided by its peak value from the load table, the two squares are summed, and the sum is multiplied by a clipped measure of contact patch velocity and then biased downward and clamped, so a wheel that is merely rolling reports nothing. The result drives a skid-mark emitter per wheel, which accumulates energy as a blend of the previous energy and the current intensity rather than following it directly. A mark is only laid, and only kept alive, once that energy exceeds a fixed threshold, so light cornering scrubs nothing while a locked wheel or a big slide immediately draws a ribbon. Body and wheel visuals are refreshed from the same physics read-back so the marks appear under the wheels that are actually sliding.", + "constants": [ + { + "name": "emissionEnergyThreshold", + "value": 5, + "unit": "accumulated squeal energy before a mark is laid, evaluated every 1/90 s tick", + "source": "src/skidmarks.h:84" + }, + { + "name": "emitterDecay", + "value": 0.9, + "unit": "retained fraction of emitter energy per tick, blended every 1/90 s tick", + "source": "src/skidmarks.cpp:96" + }, + { + "name": "skidMarkBufferSlots", + "value": 1024, + "unit": "skid-mark segments retained in the ring buffer, restored when the system resets, no per-tick rate", + "source": "src/skidmarks.h:47" + }, + { + "name": "squealBias", + "value": 0.4, + "unit": "offset subtracted from the squeal product before clamping, applied every 1/90 s tick", + "source": "src/physics/cardynamics.cpp:1003" + } + ], + "sources": [ + { + "path": "src/skidmarks.cpp", + "line": 86 + }, + { + "path": "src/physics/cardynamics.cpp", + "line": 992 + }, + { + "path": "src/game.cpp", + "line": 2537 + }, + { + "path": "src/cargraphics.cpp", + "line": 368 + } + ], + "capabilities": [ + "tyre squeal", + "skid marks", + "energy accumulation", + "ring buffer", + "visual read-back" + ], + "tags": [ + "visuals", + "effects", + "tyres", + "feedback" + ] + }, + { + "id": "replay-keyframes", + "node": "replay", + "name": "Replay input deltas and keyframes", + "summary": "Replays store only changed inputs plus periodic car state snapshots, replayed by re-running the same dynamics", + "behavior": "A replay is not a recorded animation. While recording, each car writes an input frame only when at least one control value differs from the previous stored value, so long periods of steady throttle cost almost nothing, and every thirtieth frame it also writes a full state snapshot with the serialised car state and the input vector at that instant. During playback the recorded inputs are fed back into the same car dynamics at the same fixed step, and the periodic snapshots are used to seek or to resynchronise. Because the simulation is deterministic at a fixed tick and the input stream is exact, the replayed car follows the original line without any position stream at all, and seek is a matter of restoring the nearest snapshot and replaying forward.", + "constants": [ + { + "name": "keyframeInterval", + "value": 30, + "unit": "recorded frames between full state snapshots, at one recorded frame per 1/90 s tick", + "source": "src/replay.cpp:145" + } + ], + "sources": [ + { + "path": "src/replay.cpp", + "line": 112 + }, + { + "path": "src/replay.cpp", + "line": 127 + }, + { + "path": "src/replay.cpp", + "line": 141 + } + ], + "capabilities": [ + "input delta compression", + "state keyframes", + "deterministic playback", + "seek" + ], + "tags": [ + "replay", + "netcode", + "determinism", + "data" + ] + } + ] +}