mirror of
https://github.com/IS1DI/x0gp.git
synced 2026-07-17 04:57:55 +00:00
This commit captures the full server code accumulated across
several development sessions. It includes the following layers,
applied in roughly this order during development:
1. Base infrastructure
- cmd/poc-server HTTP+WebSocket server, /health, /stats
- internal/transport wire types (Envelope + all message types)
- internal/catalog, internal/realtime, internal/stats
- internal/storage/postgres with migrations 001-004
- .env, .env.example, docker-compose, Dockerfile, scripts/
2. Swagger documentation
- go get github.com/swaggo/http-swagger
- swag init produces docs/docs.go
- /swagger/index.html UI, /swagger/doc.json spec
- annotations on health/stats/ws and catalog handlers
3. Races API
- internal/races/{store,service,keyset,types}.go
- migration 005_races.sql: finished_races, race_plans, race_queue
- GET /api/races with keyset pagination, status filter
- GET /api/races/upcoming
- POST /api/races/queue/join, GET, DELETE
- POST/GET /api/races/plans, DELETE /{id}
- lobby.RaceMeta simplification (no host_id/host_name)
4. Races seeder
- internal/races/seed with deterministic generator
- --seed-races / --reset CLI flags in main.go
- 30 finished + 5 live + 5 plans + 4 queue entries
5. Drivers and clans
- internal/drivers, internal/clans (CRUD, validation)
- migration 008_drivers_clans.sql
- /api/clans and /api/drivers endpoints
- 3-letter uppercase nickname/tag validation
- lobby.DriverMeta: Nickname, AvatarURL, ClanID, ClanTag
6. Podium
- internal/transport.RacePodiumEntry
- lobby.SetDriverProfile for in-memory metadata sync
- migration 007_podium.sql (podium JSONB column)
- seeder populates top-3 per finished race
7. Live races persistence
- migration 009_live_persistence.sql: live_races,
live_race_drivers, lobby_drivers
- internal/races/live_store.go: LiveStore with write-side
mirror for lobby.Service mutations
- Service.collectLive and Upcoming read from Postgres
- main.go RestoreFromDB rehydrates lobby on startup
8. Unify live + finished into one races table
- migration 010_unify_races.sql: rename finished_races to
races, expand status CHECK, merge live rows
- PgStore now hosts both write paths; LiveStore is a
thin facade implementing lobby.Persistence
- seeder resetAll drops only finished/cancelled rows and
race_plans / race_queue / lobby_drivers
Each layer is consistent on its own; cross-layer changes are
visible in the file history. A future refactor may split this
commit into the per-stage boundaries listed above.
614 lines
22 KiB
Go
614 lines
22 KiB
Go
// 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 {
|
||
return []Track{
|
||
Monaco(),
|
||
BarcelonaCatalunya(),
|
||
AustriaRedBullRing(),
|
||
GreatBritainSilverstone(),
|
||
BelgiumSpa(),
|
||
}
|
||
}
|
||
|
||
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
|
||
} |