ACPlayer_Webpage/components/live/LiveTrackMap.tsx

202 lines
6.8 KiB
TypeScript

// components/live/LiveTrackMap.tsx
'use client';
import { useEffect, useState } from 'react';
import { getTrackMapUrl, cleanTrackName, cleanTrackConfig } from '@/lib/trackUtils';
import { getTrackMapConfig, worldToMapCoords, type TrackMapConfig } from '@/lib/trackMapConfig';
interface Car {
carID: number;
driver_name: string;
car_model: string;
normalizedSplinePos: number;
position: number;
lap_time?: number;
best_lap_time?: number;
world_position?: { x: number; y: number; z: number };
}
interface LiveTrackMapProps {
track: string;
trackConfig: string;
cars: Car[];
}
export default function LiveTrackMap({ track, trackConfig, cars }: LiveTrackMapProps) {
const [imageError, setImageError] = useState(false);
const [mapConfig, setMapConfig] = useState<TrackMapConfig | null>(null);
// Static track bounds - set once and never change
const [trackBounds, setTrackBounds] = useState<{
minX: number;
maxX: number;
minZ: number;
maxZ: number;
} | null>(null);
// Add mouse position state
const [mousePos, setMousePos] = useState<{ x: number; y: number } | null>(null);
// Define event handlers
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
const rect = e.currentTarget.getBoundingClientRect();
const x = ((e.clientX - rect.left) / rect.width) * 100;
const y = ((e.clientY - rect.top) / rect.height) * 100;
setMousePos({ x, y });
};
const handleMouseLeave = () => {
setMousePos(null);
};
const trackMapUrl = getTrackMapUrl(track, trackConfig);
const displayTrackName = cleanTrackName(track);
const displayTrackConfig = cleanTrackConfig(trackConfig);
useEffect(() => {
getTrackMapConfig(track, trackConfig).then(config => {
setMapConfig(config);
if (config) {
const halfWidth = config.width / 2;
const halfHeight = config.height / 2;
setTrackBounds({
minX: config.xOffset - halfWidth,
maxX: config.xOffset + halfWidth,
minZ: config.zOffset - halfHeight,
maxZ: config.zOffset + halfHeight,
});
}
});
}, [track, trackConfig]);
// Convert world position to screen position
const getCarPosition = (car: Car) => {
// Use AC's formula with map.ini config
if (car.world_position && car.world_position.x !== undefined && mapConfig) {
const pos = worldToMapCoords(
car.world_position.x,
car.world_position.y,
car.world_position.z,
mapConfig
);
// Debug first car only
if (cars.indexOf(car) === 0) {
console.log(`[Map] World: (${car.world_position.x.toFixed(1)}, ${car.world_position.z.toFixed(1)}) -> Screen: (${pos.x.toFixed(1)}%, ${pos.y.toFixed(1)}%)`);
}
return pos;
}
// Fallback: distribute along diagonal based on normalizedSplinePos
const fallbackX = car.normalizedSplinePos * 100;
const fallbackY = car.normalizedSplinePos * 100;
return { x: fallbackX, y: fallbackY };
};
// Get color based on position
const getPositionColor = (position: number) => {
if (!position || position === 0) return '#6b7280'; // Gray for no position
if (position === 1) return '#ffffff'; // P1 - White
if (position === 2) return '#d1d5db'; // P2 - Light gray
if (position === 3) return '#9ca3af'; // P3 - Gray
return '#6b7280'; // Others - Dark gray
};
return (
<div
className="relative w-full aspect-square bg-black border border-white/10 overflow-hidden"
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
>
{/* Track Map Background */}
{!imageError ? (
<img
src={trackMapUrl}
alt={`${track} track map`}
className="absolute inset-0 w-full h-full object-contain opacity-60"
onError={() => setImageError(true)}
/>
) : (
<div className="absolute inset-0 flex items-center justify-center text-white/40 text-sm">
Track map not available
</div>
)}
{/* Grid overlay for reference */}
<div className="absolute inset-0 opacity-10">
<svg width="100%" height="100%" className="text-white">
<defs>
<pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
<path d="M 40 0 L 0 0 0 40" fill="none" stroke="currentColor" strokeWidth="0.5"/>
</pattern>
</defs>
<rect width="100%" height="100%" fill="url(#grid)" />
</svg>
</div>
{/* Car Positions */}
<div className="absolute inset-0">
{cars.map((car) => {
const pos = getCarPosition(car);
const color = getPositionColor(car.position);
return (
<div
key={car.carID}
className="absolute transition-all duration-300 ease-linear"
style={{
left: `${pos.x}%`,
top: `${pos.y}%`,
transform: 'translate(-50%, -50%)',
}}
>
{/* Car dot */}
<div
className="w-4 h-4 rounded-full border-2 animate-pulse"
style={{
backgroundColor: color,
borderColor: color,
boxShadow: `0 0 10px ${color}`,
}}
/>
{/* Position number */}
{car.position > 0 && (
<div
className="absolute -top-6 left-1/2 transform -translate-x-1/2 text-xs font-bold whitespace-nowrap px-2 py-1 bg-black/80 border"
style={{ borderColor: color, color: color }}
>
P{car.position}
</div>
)}
{/* Driver name on hover */}
<div className="absolute top-6 left-1/2 transform -translate-x-1/2 opacity-0 hover:opacity-100 transition-opacity whitespace-nowrap">
<div className="px-2 py-1 bg-black/90 border border-white/20 text-xs">
{car.driver_name}
</div>
</div>
</div>
);
})}
</div>
{/* Track info overlay */}
<div className="absolute top-4 left-4 bg-black/80 border border-white/20 px-3 py-2">
<div className="text-xs font-bold tracking-wider text-white/60">TRACK</div>
<div className="text-sm font-mono">{track}</div>
{trackConfig && trackConfig !== 'default' && (
<div className="text-xs text-white/60">{trackConfig}</div>
)}
</div>
{/* Live indicator */}
<div className="absolute top-4 right-4 flex items-center space-x-2 bg-black/80 border border-white/20 px-3 py-2">
<div className="w-2 h-2 bg-red-500 rounded-full animate-pulse"></div>
<span className="text-xs font-bold tracking-wider">LIVE</span>
</div>
</div>
);
}