Files

624 lines
22 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package seeddefs holds the canonical seed definitions for tracks and
// cars. It lives in its own package (no postgres dependencies) so both
// the catalog tests and the scripts/genseed SQL generator can consume
// the same source-of-truth data.
package seeddefs
import "math"
// Default tracks and cars shipped with the PoC. All entries are
// visibility=system and therefore immutable from the API.
//
// Tracks: five real F1 circuits (2024 calendar), simplified to fit a
// 4.5 × 3.2 m bounding box at 1/27 scale. Each track keeps a
// recognisable shape and characteristic corners.
//
// Cars: five real F1 2024 teams. Length / width / weight are real
// (1/24 scale), power figures are real (full-scale 1.6L V6 hybrid
// turbo, ~1000 hp combined MGU-K + ICE).
func DefaultTracks() []Track {
tracks := []Track{
Monaco(),
BarcelonaCatalunya(),
AustriaRedBullRing(),
GreatBritainSilverstone(),
BelgiumSpa(),
}
for i := range tracks {
for j := range tracks[i].Centerline {
tracks[i].Centerline[j].Y = -tracks[i].Centerline[j].Y
tracks[i].Centerline[j].HeadingRad = -tracks[i].Centerline[j].HeadingRad
}
tracks[i].Bounds = ComputeBounds(tracks[i].Centerline)
tracks[i].LengthM = tracks[i].Bounds.LengthM
tracks[i].WidthM = tracks[i].Bounds.WidthM
}
return tracks
}
func DefaultCars() []Car {
return []Car{
RedBullRB20(),
FerrariSF24(),
McLarenMCL38(),
MercedesW15(),
AstonMartinAMR24(),
}
}
// ---------------------------------------------------------------------------
// Tracks
//
// Coordinates: lower-left origin, metres. Bounds must fit inside a
// 4.5 × 3.2 m room.
//
// Conventions:
// - closed loop, samples[i] and samples[0] are not duplicated
// - speed_ms is a suggested cornering speed (lower = tighter)
// - curvature = 1/radius; 0 = straight
// ---------------------------------------------------------------------------
// Monaco — narrow street circuit, hairpins, tight corners.
// Length ≈ 3.337 km IRL → simplified to 4.0 × 2.6 m.
func Monaco() Track {
const (
lengthM = 4.0
widthM = 2.6
)
// Famous corners: Sainte-Devote, Casino Square, Mirabeau, Loews hairpin,
// Portier, Tunnel, Nouvelle chicane, Tabac, Swimming pool, Rascasse, Anthony Noghes.
// Lower speeds through Monaco → 1.8..3.2 m/s.
wps := []Waypoint{
// Start/finish straight (Sainte-Devote approach).
{X: 0.4, Y: 2.2, HeadingRad: 0, SpeedMs: 3.2, Curvature: 0},
{X: 1.1, Y: 2.2, HeadingRad: 0, SpeedMs: 3.0, Curvature: 0},
// Sainte-Devote (right kink).
{X: 1.5, Y: 2.05, HeadingRad: -0.4, SpeedMs: 2.4, Curvature: 1.2},
{X: 1.7, Y: 1.85, HeadingRad: -0.9, SpeedMs: 2.2, Curvature: 1.8},
// Up the hill to Casino / Massenet.
{X: 1.55, Y: 1.55, HeadingRad: -1.6, SpeedMs: 2.0, Curvature: 1.4},
{X: 1.25, Y: 1.4, HeadingRad: -2.4, SpeedMs: 2.1, Curvature: 1.2},
// Casino Square (slow left).
{X: 0.95, Y: 1.55, HeadingRad: math.Pi, SpeedMs: 1.9, Curvature: 1.6},
// Mirabeau hairpin (right).
{X: 0.75, Y: 1.85, HeadingRad: 2.0, SpeedMs: 1.8, Curvature: 2.2},
// Loews hairpin (tight left).
{X: 0.95, Y: 2.15, HeadingRad: 2.6, SpeedMs: 1.8, Curvature: 2.6},
{X: 1.2, Y: 2.4, HeadingRad: -2.8, SpeedMs: 2.1, Curvature: 1.4},
// Portier (right kink onto the straight).
{X: 1.6, Y: 2.5, HeadingRad: -3.0, SpeedMs: 2.4, Curvature: 1.0},
// Tunnel straight.
{X: 2.6, Y: 2.5, HeadingRad: 0, SpeedMs: 3.2, Curvature: 0},
{X: 3.4, Y: 2.5, HeadingRad: 0, SpeedMs: 3.2, Curvature: 0},
// Nouvelle chicane (fast left-right).
{X: 3.6, Y: 2.25, HeadingRad: -0.5, SpeedMs: 2.6, Curvature: 1.5},
{X: 3.55, Y: 1.95, HeadingRad: 0.4, SpeedMs: 2.6, Curvature: 1.5},
// Tabac (right).
{X: 3.3, Y: 1.75, HeadingRad: 1.0, SpeedMs: 2.3, Curvature: 1.4},
// Swimming pool (left-right-left).
{X: 2.95, Y: 1.55, HeadingRad: 1.7, SpeedMs: 2.1, Curvature: 1.8},
{X: 2.6, Y: 1.65, HeadingRad: 2.6, SpeedMs: 2.1, Curvature: 1.8},
{X: 2.4, Y: 1.9, HeadingRad: -2.7, SpeedMs: 2.3, Curvature: 1.4},
// Rascasse (left).
{X: 2.4, Y: 2.2, HeadingRad: -3.0, SpeedMs: 2.2, Curvature: 1.6},
// Anthony Noghes (left onto pit straight).
{X: 2.6, Y: 2.4, HeadingRad: 2.8, SpeedMs: 2.5, Curvature: 1.0},
// Pit straight (back to start).
{X: 3.2, Y: 2.4, HeadingRad: 0, SpeedMs: 3.2, Curvature: 0},
{X: 3.7, Y: 2.4, HeadingRad: 0, SpeedMs: 3.2, Curvature: 0},
{X: 3.95, Y: 2.3, HeadingRad: -0.3, SpeedMs: 2.8, Curvature: 0.8},
}
b := ComputeBounds(wps)
return Track{
ID: "monaco",
Name: "Monaco",
Description: "Circuit de Monaco. Narrow street circuit, tight hairpins, no room for error.",
AuthorID: "system",
Visibility: "system",
Scale: 27,
LengthM: b.LengthM,
WidthM: b.WidthM,
LaneWidthM: 0.18,
Surface: "tile",
Tags: []string{"f1", "street", "tight", "slow", "monaco"},
Centerline: wps,
Bounds: b,
BestLapHolder: "Verstappen",
}
}
// BarcelonaCatalunya — mix of high-speed and technical, used for testing.
// Length ≈ 4.675 km IRL → 4.3 × 2.9 m.
func BarcelonaCatalunya() Track {
const (
lengthM = 4.3
widthM = 2.9
)
wps := []Waypoint{
// Main straight.
{X: 0.3, Y: 0.4, HeadingRad: 0, SpeedMs: 4.5, Curvature: 0},
{X: 1.4, Y: 0.4, HeadingRad: 0, SpeedMs: 4.5, Curvature: 0},
// Turn 1 — Elf (right hairpin).
{X: 1.7, Y: 0.65, HeadingRad: math.Pi / 2, SpeedMs: 2.2, Curvature: 1.6},
{X: 1.5, Y: 0.95, HeadingRad: math.Pi, SpeedMs: 2.0, Curvature: 2.0},
// Turn 2-3 — right kink.
{X: 1.2, Y: 1.1, HeadingRad: math.Pi + 0.3, SpeedMs: 2.6, Curvature: 1.0},
{X: 0.95, Y: 1.0, HeadingRad: math.Pi + 0.9, SpeedMs: 2.6, Curvature: 1.0},
// Turn 4 — Repsol (left).
{X: 0.8, Y: 1.2, HeadingRad: -math.Pi / 2, SpeedMs: 2.4, Curvature: 1.2},
// Turn 5 — fast right.
{X: 1.05, Y: 1.5, HeadingRad: 0, SpeedMs: 3.2, Curvature: 0.7},
// Turn 7-8 — slow left-right.
{X: 1.3, Y: 1.65, HeadingRad: math.Pi / 2, SpeedMs: 2.4, Curvature: 1.0},
{X: 1.15, Y: 1.85, HeadingRad: math.Pi, SpeedMs: 2.4, Curvature: 1.0},
// Turn 9-10 — Campsa (fast right).
{X: 1.4, Y: 2.0, HeadingRad: math.Pi - 0.5, SpeedMs: 3.0, Curvature: 0.7},
{X: 1.7, Y: 2.0, HeadingRad: math.Pi - 1.0, SpeedMs: 3.0, Curvature: 0.7},
// Turn 12-13 — long right.
{X: 1.95, Y: 2.2, HeadingRad: math.Pi / 2, SpeedMs: 2.8, Curvature: 0.5},
{X: 1.65, Y: 2.5, HeadingRad: 0.3, SpeedMs: 2.8, Curvature: 0.5},
// Turn 14-15 — fast right curve.
{X: 2.2, Y: 2.7, HeadingRad: 0.7, SpeedMs: 3.2, Curvature: 0.5},
{X: 2.8, Y: 2.7, HeadingRad: 1.1, SpeedMs: 3.4, Curvature: 0.5},
// Turn 16 — La Caixa (right hairpin).
{X: 3.1, Y: 2.5, HeadingRad: math.Pi / 2 + 0.4, SpeedMs: 2.2, Curvature: 1.4},
{X: 2.95, Y: 2.2, HeadingRad: math.Pi + 0.4, SpeedMs: 2.2, Curvature: 1.4},
// Back straight.
{X: 3.3, Y: 1.9, HeadingRad: 0, SpeedMs: 4.0, Curvature: 0},
{X: 4.0, Y: 1.9, HeadingRad: 0, SpeedMs: 4.0, Curvature: 0},
// Turn 16bis-17 (chicane before start).
{X: 4.05, Y: 1.6, HeadingRad: -math.Pi / 2, SpeedMs: 2.4, Curvature: 1.4},
{X: 3.8, Y: 1.4, HeadingRad: math.Pi, SpeedMs: 2.4, Curvature: 1.4},
{X: 3.5, Y: 1.5, HeadingRad: math.Pi + math.Pi/4, SpeedMs: 2.6, Curvature: 1.0},
// Approach to start.
{X: 3.1, Y: 1.25, HeadingRad: -math.Pi / 2 - 0.4, SpeedMs: 3.0, Curvature: 0.7},
{X: 2.7, Y: 1.0, HeadingRad: -math.Pi / 2 - 0.9, SpeedMs: 3.0, Curvature: 0.7},
{X: 2.3, Y: 0.7, HeadingRad: math.Pi + 0.4, SpeedMs: 3.4, Curvature: 0.5},
{X: 1.6, Y: 0.5, HeadingRad: math.Pi + 0.2, SpeedMs: 3.6, Curvature: 0.4},
{X: 0.9, Y: 0.42, HeadingRad: 0, SpeedMs: 4.0, Curvature: 0},
}
b := ComputeBounds(wps)
return Track{
ID: "barcelona-catalunya",
Name: "Barcelona-Catalunya",
Description: "Circuit de Barcelona-Catalunya. Mix of high-speed corners and technical chicanes.",
AuthorID: "system",
Visibility: "system",
Scale: 27,
LengthM: b.LengthM,
WidthM: b.WidthM,
LaneWidthM: 0.18,
Surface: "tile",
Tags: []string{"f1", "balanced", "testing", "barcelona"},
Centerline: wps,
Bounds: b,
BestLapHolder: "Verstappen",
}
}
// AustriaRedBullRing — short, fast, lots of elevation IRL.
// Length ≈ 4.318 km IRL → 4.0 × 2.4 m.
func AustriaRedBullRing() Track {
const (
lengthM = 4.0
widthM = 2.4
)
wps := []Waypoint{
// Main straight.
{X: 0.3, Y: 0.4, HeadingRad: 0, SpeedMs: 4.8, Curvature: 0},
{X: 1.6, Y: 0.4, HeadingRad: 0, SpeedMs: 4.8, Curvature: 0},
// Turn 1 — right hairpin.
{X: 1.85, Y: 0.65, HeadingRad: math.Pi / 2, SpeedMs: 2.0, Curvature: 1.8},
{X: 1.6, Y: 0.95, HeadingRad: math.Pi, SpeedMs: 2.0, Curvature: 1.8},
// Turn 2 — fast right.
{X: 1.3, Y: 1.0, HeadingRad: math.Pi + 0.5, SpeedMs: 3.0, Curvature: 0.7},
// Turn 3 — left.
{X: 1.05, Y: 1.2, HeadingRad: -math.Pi / 2, SpeedMs: 2.6, Curvature: 1.0},
// Turn 4 — Remus (fast uphill right).
{X: 1.4, Y: 1.4, HeadingRad: 0, SpeedMs: 3.2, Curvature: 0.5},
{X: 1.85, Y: 1.4, HeadingRad: math.Pi / 2 + 0.2, SpeedMs: 3.2, Curvature: 0.5},
// Turn 5-6 — fast right-left.
{X: 2.1, Y: 1.65, HeadingRad: math.Pi, SpeedMs: 3.0, Curvature: 0.7},
{X: 2.0, Y: 1.85, HeadingRad: -math.Pi / 2, SpeedMs: 3.0, Curvature: 0.7},
// Turn 7 — right hairpin.
{X: 2.25, Y: 2.05, HeadingRad: 0, SpeedMs: 2.0, Curvature: 1.8},
{X: 2.4, Y: 2.0, HeadingRad: math.Pi / 2, SpeedMs: 2.0, Curvature: 1.8},
// Turn 8 — left hairpin.
{X: 2.4, Y: 2.2, HeadingRad: math.Pi, SpeedMs: 2.0, Curvature: 1.8},
{X: 2.2, Y: 2.3, HeadingRad: -math.Pi / 2, SpeedMs: 2.0, Curvature: 1.8},
// Turn 9 — back straight start (left kink).
{X: 2.55, Y: 2.4, HeadingRad: -0.2, SpeedMs: 3.4, Curvature: 0.4},
{X: 3.0, Y: 2.4, HeadingRad: 0, SpeedMs: 4.0, Curvature: 0},
{X: 3.5, Y: 2.4, HeadingRad: 0, SpeedMs: 4.5, Curvature: 0},
// Turn 10 — fast right.
{X: 3.85, Y: 2.15, HeadingRad: math.Pi / 2 + 0.2, SpeedMs: 3.4, Curvature: 0.6},
// Back to pit straight via Schumacher esses.
{X: 3.6, Y: 1.8, HeadingRad: math.Pi, SpeedMs: 3.0, Curvature: 0.8},
{X: 3.25, Y: 1.85, HeadingRad: math.Pi + 0.6, SpeedMs: 3.0, Curvature: 0.8},
{X: 3.0, Y: 1.6, HeadingRad: -math.Pi / 2 - 0.4, SpeedMs: 3.2, Curvature: 0.6},
{X: 2.6, Y: 1.4, HeadingRad: -math.Pi, SpeedMs: 3.4, Curvature: 0.5},
{X: 2.0, Y: 1.2, HeadingRad: math.Pi - 0.5, SpeedMs: 3.6, Curvature: 0.4},
{X: 1.4, Y: 0.8, HeadingRad: math.Pi - 0.2, SpeedMs: 3.8, Curvature: 0.3},
{X: 0.8, Y: 0.5, HeadingRad: 0, SpeedMs: 4.2, Curvature: 0.2},
}
b := ComputeBounds(wps)
return Track{
ID: "austria-redbull-ring",
Name: "Austria (Red Bull Ring)",
Description: "Red Bull Ring. Short, fast, three straights separated by hairpins.",
AuthorID: "system",
Visibility: "system",
Scale: 27,
LengthM: b.LengthM,
WidthM: b.WidthM,
LaneWidthM: 0.18,
Surface: "wood",
Tags: []string{"f1", "short", "fast", "austria"},
Centerline: wps,
Bounds: b,
BestLapHolder: "Verstappen",
}
}
// GreatBritainSilverstone — fast, flowing, essex corners.
// Length ≈ 5.891 km IRL → 4.4 × 3.0 m.
func GreatBritainSilverstone() Track {
const (
lengthM = 4.4
widthM = 3.0
)
wps := []Waypoint{
// Main straight (Wellington / pit straight), bottom of room.
{X: 0.3, Y: 0.3, HeadingRad: 0, SpeedMs: 5.0, Curvature: 0},
{X: 2.4, Y: 0.3, HeadingRad: 0, SpeedMs: 5.0, Curvature: 0},
// Turn 1 — Abbey (right).
{X: 2.8, Y: 0.45, HeadingRad: math.Pi / 2, SpeedMs: 3.0, Curvature: 0.8},
// Turn 2 — Farm Curve (left).
{X: 2.6, Y: 0.8, HeadingRad: math.Pi, SpeedMs: 3.4, Curvature: 0.6},
// Turn 3 — Village (right kink).
{X: 2.95, Y: 1.05, HeadingRad: math.Pi / 2 + 0.5, SpeedMs: 3.2, Curvature: 0.7},
// Turn 4 — The Loop (right hairpin).
{X: 3.3, Y: 1.3, HeadingRad: 0, SpeedMs: 2.2, Curvature: 1.6},
{X: 3.6, Y: 1.15, HeadingRad: -math.Pi / 2, SpeedMs: 2.2, Curvature: 1.6},
// Turn 5 — Aintree (fast left).
{X: 3.6, Y: 0.8, HeadingRad: -math.Pi, SpeedMs: 3.0, Curvature: 0.7},
// Turn 6 — Brooklands (right).
{X: 3.85, Y: 0.55, HeadingRad: -math.Pi / 2, SpeedMs: 2.6, Curvature: 1.0},
// Turn 7 — Luffield (long right).
{X: 3.55, Y: 0.25, HeadingRad: 0, SpeedMs: 2.4, Curvature: 1.0},
{X: 3.25, Y: 0.25, HeadingRad: math.Pi / 2, SpeedMs: 2.4, Curvature: 1.0},
// Turn 8-9 — Woodcote (right kink onto pit straight).
{X: 3.05, Y: 0.55, HeadingRad: math.Pi / 2 + 0.3, SpeedMs: 3.4, Curvature: 0.5},
{X: 2.85, Y: 0.45, HeadingRad: math.Pi, SpeedMs: 4.0, Curvature: 0.3},
// (mid-pit straight into Copse corner)
{X: 1.8, Y: 0.3, HeadingRad: 0, SpeedMs: 4.6, Curvature: 0},
// Copse (fast right).
{X: 1.2, Y: 0.45, HeadingRad: -math.Pi / 2 - 0.2, SpeedMs: 3.8, Curvature: 0.4},
// Maggotts (right curve, swept).
{X: 1.0, Y: 0.85, HeadingRad: -math.Pi - 0.4, SpeedMs: 3.6, Curvature: 0.5},
// Beckets (left hairpin-ish).
{X: 0.7, Y: 1.15, HeadingRad: math.Pi / 2 + 0.3, SpeedMs: 2.4, Curvature: 1.2},
{X: 0.5, Y: 0.9, HeadingRad: 0, SpeedMs: 2.6, Curvature: 0.9},
// Chapel (right kink).
{X: 0.7, Y: 0.65, HeadingRad: -math.Pi / 2, SpeedMs: 3.0, Curvature: 0.7},
// Hangar (left).
{X: 0.45, Y: 0.45, HeadingRad: -math.Pi - 0.4, SpeedMs: 2.6, Curvature: 1.0},
// Stowe (right).
{X: 0.75, Y: 1.0, HeadingRad: math.Pi / 2 - 0.4, SpeedMs: 2.4, Curvature: 1.0},
// Vale (left kink).
{X: 0.5, Y: 1.5, HeadingRad: -math.Pi - 0.4, SpeedMs: 2.6, Curvature: 0.8},
// Club (right kink onto Wellington).
{X: 0.3, Y: 1.15, HeadingRad: -math.Pi / 2, SpeedMs: 2.4, Curvature: 1.0},
// Top return straight + kink back to Wellington.
{X: 0.7, Y: 1.6, HeadingRad: math.Pi / 4, SpeedMs: 3.4, Curvature: 0.5},
{X: 2.0, Y: 1.95, HeadingRad: math.Pi / 2 + 0.4, SpeedMs: 3.0, Curvature: 0.7},
{X: 3.0, Y: 2.4, HeadingRad: math.Pi + 0.2, SpeedMs: 3.4, Curvature: 0.4},
{X: 2.4, Y: 2.65, HeadingRad: -math.Pi / 2 - 0.3, SpeedMs: 4.0, Curvature: 0.3},
{X: 1.4, Y: 2.6, HeadingRad: -math.Pi, SpeedMs: 4.4, Curvature: 0.2},
{X: 0.5, Y: 2.3, HeadingRad: -math.Pi + 0.3, SpeedMs: 4.8, Curvature: 0.2},
{X: 0.3, Y: 1.7, HeadingRad: -math.Pi / 2 + 0.3, SpeedMs: 5.0, Curvature: 0.1},
}
b := ComputeBounds(wps)
return Track{
ID: "great-britain-silverstone",
Name: "Great Britain (Silverstone)",
Description: "Silverstone Circuit. Fast, flowing corners; home of British motorsport.",
AuthorID: "system",
Visibility: "system",
Scale: 27,
LengthM: b.LengthM,
WidthM: b.WidthM,
LaneWidthM: 0.18,
Surface: "carpet",
Tags: []string{"f1", "fast", "flowing", "silverstone"},
Centerline: wps,
Bounds: b,
BestLapHolder: "Hamilton",
}
}
// BelgiumSpa — long, fast, Eau Rouge / Raidillon, varied weather.
// Length ≈ 7.004 km IRL → 4.5 × 3.0 m.
func BelgiumSpa() Track {
const (
lengthM = 4.5
widthM = 3.0
)
wps := []Waypoint{
// Main straight (La Source exit).
{X: 0.4, Y: 0.5, HeadingRad: 0, SpeedMs: 4.8, Curvature: 0},
{X: 1.6, Y: 0.5, HeadingRad: 0, SpeedMs: 4.8, Curvature: 0},
// Eau Rouge / Raidillon (fast uphill left-right-left).
{X: 1.95, Y: 0.6, HeadingRad: 0.3, SpeedMs: 4.2, Curvature: 0.5},
{X: 2.2, Y: 0.95, HeadingRad: 0.9, SpeedMs: 4.0, Curvature: 0.6},
{X: 2.1, Y: 1.3, HeadingRad: math.Pi + 0.4, SpeedMs: 4.0, Curvature: 0.6},
// Kemmel straight.
{X: 2.7, Y: 1.5, HeadingRad: 0, SpeedMs: 5.0, Curvature: 0},
{X: 3.5, Y: 1.5, HeadingRad: 0, SpeedMs: 5.0, Curvature: 0},
// Les Combes (chicane).
{X: 3.85, Y: 1.3, HeadingRad: -math.Pi / 2, SpeedMs: 2.4, Curvature: 1.4},
{X: 3.6, Y: 1.1, HeadingRad: math.Pi + 0.3, SpeedMs: 2.6, Curvature: 1.2},
{X: 3.3, Y: 1.25, HeadingRad: math.Pi + 0.9, SpeedMs: 2.6, Curvature: 1.2},
// Malmedy (fast right sweep).
{X: 3.5, Y: 1.55, HeadingRad: math.Pi / 2, SpeedMs: 3.6, Curvature: 0.4},
// Rivage (right hairpin).
{X: 3.25, Y: 1.85, HeadingRad: 0, SpeedMs: 2.2, Curvature: 1.6},
{X: 3.05, Y: 1.7, HeadingRad: -math.Pi / 2, SpeedMs: 2.4, Curvature: 1.4},
// Campus (left kink).
{X: 3.25, Y: 1.5, HeadingRad: 0, SpeedMs: 3.0, Curvature: 0.7},
// Stavelot (fast right).
{X: 3.7, Y: 1.65, HeadingRad: math.Pi / 2 + 0.3, SpeedMs: 3.4, Curvature: 0.5},
// Up to Blanchimont (long right).
{X: 4.1, Y: 2.0, HeadingRad: math.Pi / 2 + 0.6, SpeedMs: 4.0, Curvature: 0.3},
// Blanchimont (very fast right kink).
{X: 4.25, Y: 2.4, HeadingRad: math.Pi + 0.4, SpeedMs: 4.6, Curvature: 0.3},
// Bus stop chicane (slow left-right).
{X: 4.0, Y: 2.7, HeadingRad: -math.Pi / 2 - 0.3, SpeedMs: 2.4, Curvature: 1.4},
{X: 3.7, Y: 2.55, HeadingRad: math.Pi, SpeedMs: 2.4, Curvature: 1.4},
{X: 3.55, Y: 2.75, HeadingRad: math.Pi + 0.7, SpeedMs: 2.4, Curvature: 1.4},
// Back to La Source hairpin.
{X: 3.05, Y: 2.7, HeadingRad: -math.Pi / 2, SpeedMs: 2.0, Curvature: 1.8},
{X: 2.85, Y: 2.45, HeadingRad: 0, SpeedMs: 2.0, Curvature: 1.8},
// Pit straight.
{X: 2.0, Y: 2.2, HeadingRad: math.Pi - 0.3, SpeedMs: 3.6, Curvature: 0.3},
{X: 1.2, Y: 1.6, HeadingRad: math.Pi - 0.6, SpeedMs: 4.0, Curvature: 0.2},
{X: 0.6, Y: 1.05, HeadingRad: math.Pi - 0.3, SpeedMs: 4.4, Curvature: 0.2},
}
b := ComputeBounds(wps)
return Track{
ID: "belgium-spa",
Name: "Belgium (Spa-Francorchamps)",
Description: "Spa-Francorchamps. Long, fast, legendary Eau Rouge / Raidillon.",
AuthorID: "system",
Visibility: "system",
Scale: 27,
LengthM: b.LengthM,
WidthM: b.WidthM,
LaneWidthM: 0.18,
Surface: "carpet",
Tags: []string{"f1", "long", "fast", "spa"},
Centerline: wps,
Bounds: b,
BestLapHolder: "Verstappen",
}
}
// ---------------------------------------------------------------------------
// Cars — F1 2024 constructors
//
// Real cars are 1:1 with ~5.7 × 2.0 × 0.95 m, ~798 kg, ~1000 hp.
// We model them at 1/24 scale so the largest dimension (~238 mm) still
// fits the catalog physical-envelope check.
// ---------------------------------------------------------------------------
// RedBullRB20 — Oracle Red Bull Racing, Verstappen / Perez.
// 2024: 9 wins (last year of dominant era), Constructors' champion.
func RedBullRB20() Car {
return Car{
ID: "f1-2024-redbull-rb20",
Name: "Oracle Red Bull Racing RB20",
OwnerID: "system",
Visibility: "system",
Scale: 24,
LengthMm: 238, // 5.7m IRL
WidthMm: 83, // 2.0m IRL
HeightMm: 40, // 0.95m IRL
WeightG: 33, // 798 kg IRL
Chassis: Chassis{
Model: "RB20 carbon monocoque",
Material: "carbon-fibre",
Printed: false,
WheelbaseMm: 150, // 3.6m IRL
TrackMm: 75, // 1.8m IRL
},
Motor: Motor{
Kind: "turbo-hybrid",
Class: "F1 PU",
KV: 0,
PowerW: 746000, // ~1000 hp ICE + MGU-K, peak 746 kW
},
Battery: Battery{
VoltageV: 0,
CapacityMah: 0,
Cells: 0,
Chemistry: "li-ion MGU-K",
},
Drive: "2WD",
TopSpeedMs: 95, // ~340 km/h
ColorHex: "#1e3a8a", // dark blue + red/yellow accents
Active: true,
TotalRaces: 0,
TotalLaps: 0,
BestLapMs: 0,
}
}
// FerrariSF24 — Scuderia Ferrari, Leclerc / Sainz.
func FerrariSF24() Car {
return Car{
ID: "f1-2024-ferrari-sf24",
Name: "Scuderia Ferrari SF-24",
OwnerID: "system",
Visibility: "system",
Scale: 24,
LengthMm: 238,
WidthMm: 83,
HeightMm: 40,
WeightG: 33,
Chassis: Chassis{
Model: "SF-24 carbon monocoque",
Material: "carbon-fibre",
Printed: false,
WheelbaseMm: 150,
TrackMm: 75,
},
Motor: Motor{
Kind: "turbo-hybrid",
Class: "F1 PU 066/12",
KV: 0,
PowerW: 746000,
},
Battery: Battery{
Chemistry: "li-ion MGU-K",
},
Drive: "2WD",
TopSpeedMs: 94,
ColorHex: "#dc2626", // rosso corsa
Active: true,
}
}
// McLarenMCL38 — McLaren Racing, Norris / Piastri. 2024 Constructors' runner-up.
func McLarenMCL38() Car {
return Car{
ID: "f1-2024-mclaren-mcl38",
Name: "McLaren MCL38",
OwnerID: "system",
Visibility: "system",
Scale: 24,
LengthMm: 238,
WidthMm: 83,
HeightMm: 40,
WeightG: 33,
Chassis: Chassis{
Model: "MCL38 carbon monocoque",
Material: "carbon-fibre",
Printed: false,
WheelbaseMm: 150,
TrackMm: 75,
},
Motor: Motor{
Kind: "turbo-hybrid",
Class: "F1 PU Mercedes-derived",
KV: 0,
PowerW: 746000,
},
Battery: Battery{
Chemistry: "li-ion MGU-K",
},
Drive: "2WD",
TopSpeedMs: 94,
ColorHex: "#ea580c", // papaya orange
Active: true,
}
}
// MercedesW15 — Mercedes-AMG Petronas, Russell / Hamilton (last year of Hamilton at Mercedes).
func MercedesW15() Car {
return Car{
ID: "f1-2024-mercedes-w15",
Name: "Mercedes-AMG W15",
OwnerID: "system",
Visibility: "system",
Scale: 24,
LengthMm: 238,
WidthMm: 83,
HeightMm: 40,
WeightG: 33,
Chassis: Chassis{
Model: "W15 carbon monocoque",
Material: "carbon-fibre",
Printed: false,
WheelbaseMm: 150,
TrackMm: 75,
},
Motor: Motor{
Kind: "turbo-hybrid",
Class: "F1 PU M15",
KV: 0,
PowerW: 746000,
},
Battery: Battery{
Chemistry: "li-ion MGU-K",
},
Drive: "2WD",
TopSpeedMs: 94,
ColorHex: "#06b6d4", // silver-petronas cyan
Active: true,
}
}
// AstonMartinAMR24 — Aston Martin Aramco, Alonso / Stroll.
func AstonMartinAMR24() Car {
return Car{
ID: "f1-2024-astonmartin-amr24",
Name: "Aston Martin Aramco AMR24",
OwnerID: "system",
Visibility: "system",
Scale: 24,
LengthMm: 238,
WidthMm: 83,
HeightMm: 40,
WeightG: 33,
Chassis: Chassis{
Model: "AMR24 carbon monocoque",
Material: "carbon-fibre",
Printed: false,
WheelbaseMm: 150,
TrackMm: 75,
},
Motor: Motor{
Kind: "turbo-hybrid",
Class: "F1 PU Mercedes-derived",
KV: 0,
PowerW: 746000,
},
Battery: Battery{
Chemistry: "li-ion MGU-K",
},
Drive: "2WD",
TopSpeedMs: 94,
ColorHex: "#16a34a", // racing green
Active: true,
}
}
// ComputeBounds derives a bounding box from a closed loop of waypoints.
func ComputeBounds(loop []Waypoint) Bounds {
if len(loop) == 0 {
return Bounds{}
}
b := Bounds{
MinX: loop[0].X,
MinY: loop[0].Y,
MaxX: loop[0].X,
MaxY: loop[0].Y,
}
for _, p := range loop[1:] {
if p.X < b.MinX {
b.MinX = p.X
}
if p.Y < b.MinY {
b.MinY = p.Y
}
if p.X > b.MaxX {
b.MaxX = p.X
}
if p.Y > b.MaxY {
b.MaxY = p.Y
}
}
b.LengthM = b.MaxX - b.MinX
b.WidthM = b.MaxY - b.MinY
return b
}