mirror of
https://github.com/IS1DI/x0gp.git
synced 2026-07-16 20:47:55 +00:00
feat(server): initial implementation
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.
This commit is contained in:
@@ -0,0 +1,614 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user