From 53c36e6fc36a9a2fdbd8074802cbb4408c275681 Mon Sep 17 00:00:00 2001 From: Gavin Corkins Date: Tue, 19 May 2026 16:54:07 -0400 Subject: [PATCH] Add REBUILT game mode indicators and fix results differential data - Add gameConfig.ts with GAME_MODE constant to control game-specific features - Add AllianceIndicators component showing shift status and RP progress (Energized, Supercharged, Traversal) for REBUILT game mode - Update MatchView to read RP/shift files and display AllianceIndicators when GAME_MODE is REBUILT - Update overlayState to persist differentialData so it survives page navigation - Fix Results screen to read differentialData from overlay state instead of in-memory tracker, ensuring the points differential graph displays correctly after match ends --- next.config.ts | 2 +- package-lock.json | 2 +- src/app/lib/gameConfig.ts | 1 + src/app/lib/overlayState.ts | 2 + .../overlay/components/AllianceIndicators.tsx | 61 ++ src/app/overlay/components/MatchView.tsx | 584 +++++++++--------- src/app/overlay/components/Results.tsx | 10 +- 7 files changed, 363 insertions(+), 299 deletions(-) create mode 100644 src/app/lib/gameConfig.ts create mode 100644 src/app/overlay/components/AllianceIndicators.tsx diff --git a/next.config.ts b/next.config.ts index ebd91da..a2888ed 100644 --- a/next.config.ts +++ b/next.config.ts @@ -5,4 +5,4 @@ const nextConfig: NextConfig = { devIndicators: false, }; -export default nextConfig; +export default nextConfig; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 9730c32..225002e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6180,4 +6180,4 @@ } } } -} +} \ No newline at end of file diff --git a/src/app/lib/gameConfig.ts b/src/app/lib/gameConfig.ts new file mode 100644 index 0000000..ec5ba9c --- /dev/null +++ b/src/app/lib/gameConfig.ts @@ -0,0 +1 @@ +export const GAME_MODE: string = 'REBUILT'; \ No newline at end of file diff --git a/src/app/lib/overlayState.ts b/src/app/lib/overlayState.ts index 6bfb4b8..286521c 100644 --- a/src/app/lib/overlayState.ts +++ b/src/app/lib/overlayState.ts @@ -55,6 +55,7 @@ export interface OverlayState { field2BlueSecondaryColor?: string; field2AllianceBranding?: boolean; field2FlippedTeams?: boolean; + differentialData?: { redScore: number; blueScore: number; differential: number; gameTime: string; timestamp: number }[]; } const defaultState: OverlayState = { @@ -98,6 +99,7 @@ const defaultState: OverlayState = { field2BlueSeriesScore: 0, field2AllianceBranding: false, field2FlippedTeams: false, + differentialData: [], }; export const getOverlayState = async (): Promise => { diff --git a/src/app/overlay/components/AllianceIndicators.tsx b/src/app/overlay/components/AllianceIndicators.tsx new file mode 100644 index 0000000..0371c31 --- /dev/null +++ b/src/app/overlay/components/AllianceIndicators.tsx @@ -0,0 +1,61 @@ +interface AllianceIndicatorsProps { + isRed: boolean | undefined; + hubActive: boolean; + energized: boolean; + supercharged: boolean; + traversal: boolean; + align: 'left' | 'right'; +} + +export default function AllianceIndicators({ + isRed, hubActive, energized, supercharged, traversal, align +}: AllianceIndicatorsProps) { + const activeBg = isRed ? 'bg-red-500/80' : 'bg-blue-500/80'; + const inactiveBg = 'bg-gray-700/50'; + + const rps = [ + { label: 'โšก', name: 'Energized', active: energized }, + { label: 'โšกโšก', name: 'Supercharged', active: supercharged }, + { label: '๐Ÿ”', name: 'Traversal', active: traversal }, + ]; + + return ( +
+ {/* Shift arrow */} +
+ Shift + + {align === 'left' ? 'โ—€' : 'โ–ถ'} + +
+ + {/* RP indicators */} +
+ {rps.map(rp => ( +
+ {rp.label} + {rp.name.slice(0, 5)} +
+ ))} +
+
+ ); +} \ No newline at end of file diff --git a/src/app/overlay/components/MatchView.tsx b/src/app/overlay/components/MatchView.tsx index 6332214..4b04c09 100644 --- a/src/app/overlay/components/MatchView.tsx +++ b/src/app/overlay/components/MatchView.tsx @@ -3,6 +3,9 @@ import { useEffect, useState, useMemo } from "react"; import Image from "next/image"; import { pointsDifferentialTracker } from "../../lib/pointsDifferentialTracker"; import { Team, loadTeams } from "../../lib/teams"; +import { setOverlayState } from "../../lib/overlayState"; +import AllianceIndicators from './AllianceIndicators'; +import { GAME_MODE } from '../../lib/gameConfig'; interface MatchViewProps { state: OverlayState; @@ -10,257 +13,255 @@ interface MatchViewProps { } export default function MatchView({ state, currentTime }: MatchViewProps) { + + // โ”€โ”€โ”€ UI State โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + const isRebuilt = GAME_MODE === 'REBUILT'; const [isVisible, setIsVisible] = useState(false); + const [showAutonomous, setShowAutonomous] = useState(false); + const [showTimer, setShowTimer] = useState(false); + const [gameState, setGameState] = useState(''); + const [teams, setTeams] = useState([]); + + // โ”€โ”€โ”€ Score Animation State โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ const [prevScores, setPrevScores] = useState({ red: state.redScore, blue: state.blueScore }); const [scoreChanged, setScoreChanged] = useState({ red: false, blue: false }); const [animatingScores, setAnimatingScores] = useState({ red: state.redScore, blue: state.blueScore }); + + // โ”€โ”€โ”€ OPR Animation State โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ const [prevOPR, setPrevOPR] = useState({ red: state.redOPR, blue: state.blueOPR }); const [oprChanged, setOprChanged] = useState({ red: new Set(), blue: new Set() }); const [animatingOPR, setAnimatingOPR] = useState({ red: state.redOPR, blue: state.blueOPR }); - const [showAutonomous, setShowAutonomous] = useState(false); - const [gameState, setGameState] = useState(''); - const [showTimer, setShowTimer] = useState(false); - const [teams, setTeams] = useState([]); - // Memoize sorted OPR arrays to prevent unnecessary re-calculations - const sortedRedOPR = useMemo(() => { - return [...animatingOPR.red] - .filter(player => player.username && player.username.trim() !== '') - .sort((a, b) => b.score - a.score) - .slice(0, 3); - }, [animatingOPR.red]); - - const sortedBlueOPR = useMemo(() => { - return [...animatingOPR.blue] - .filter(player => player.username && player.username.trim() !== '') - .sort((a, b) => b.score - a.score) - .slice(0, 3); - }, [animatingOPR.blue]); + // โ”€โ”€โ”€ RP / Shift State โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + const [autoFuelRed, setAutoFuelRed] = useState(0); + const [autoFuelBlue, setAutoFuelBlue] = useState(0); + const [totalFuelRed, setTotalFuelRed] = useState(0); + const [totalFuelBlue, setTotalFuelBlue] = useState(0); + const [towerPointsRed, setTowerPointsRed] = useState(0); + const [towerPointsBlue, setTowerPointsBlue] = useState(0); - // Get teams by ID - const redTeam = useMemo(() => - teams.find(t => t.id === state.redTeamId), - [teams, state.redTeamId] - ); - - const blueTeam = useMemo(() => - teams.find(t => t.id === state.blueTeamId), - [teams, state.blueTeamId] - ); - - // Swap team data and components when flipped - const leftTeam = useMemo(() => - state.flippedTeams ? blueTeam : redTeam, - [state.flippedTeams, redTeam, blueTeam] - ); - - const rightTeam = useMemo(() => - state.flippedTeams ? redTeam : blueTeam, - [state.flippedTeams, redTeam, blueTeam] - ); - - - const leftOPR = useMemo(() => - state.flippedTeams ? sortedBlueOPR : sortedRedOPR, - [state.flippedTeams, sortedRedOPR, sortedBlueOPR] - ); - - const rightOPR = useMemo(() => - state.flippedTeams ? sortedRedOPR : sortedBlueOPR, - [state.flippedTeams, sortedRedOPR, sortedBlueOPR] - ); + // โ”€โ”€โ”€ Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + const parseTimeToSeconds = (timeStr: string): number => { + const [minutes, seconds] = timeStr.split(':').map(Number); + return (minutes || 0) * 60 + (seconds || 0); + }; - const leftOPRChanged = useMemo(() => - state.flippedTeams ? oprChanged.blue : oprChanged.red, - [state.flippedTeams, oprChanged.red, oprChanged.blue] - ); - - const rightOPRChanged = useMemo(() => - state.flippedTeams ? oprChanged.red : oprChanged.blue, - [state.flippedTeams, oprChanged.red, oprChanged.blue] + // โ”€โ”€โ”€ Memoized OPR Arrays โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + const sortedRedOPR = useMemo(() => + [...animatingOPR.red] + .filter(p => p.username && p.username.trim() !== '') + .sort((a, b) => b.score - a.score) + .slice(0, 3), + [animatingOPR.red] ); - const leftAnimatingScore = useMemo(() => - state.flippedTeams ? animatingScores.blue : animatingScores.red, - [state.flippedTeams, animatingScores.red, animatingScores.blue] - ); - - const rightAnimatingScore = useMemo(() => - state.flippedTeams ? animatingScores.red : animatingScores.blue, - [state.flippedTeams, animatingScores.red, animatingScores.blue] + const sortedBlueOPR = useMemo(() => + [...animatingOPR.blue] + .filter(p => p.username && p.username.trim() !== '') + .sort((a, b) => b.score - a.score) + .slice(0, 3), + [animatingOPR.blue] ); - const leftScoreChanged = useMemo(() => - state.flippedTeams ? scoreChanged.blue : scoreChanged.red, - [state.flippedTeams, scoreChanged.red, scoreChanged.blue] - ); - - const rightScoreChanged = useMemo(() => - state.flippedTeams ? scoreChanged.red : scoreChanged.blue, - [state.flippedTeams, scoreChanged.red, scoreChanged.blue] + // โ”€โ”€โ”€ Team Lookups โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + const redTeam = useMemo(() => + teams.find(t => t.id === state.redTeamId), + [teams, state.redTeamId] ); - const leftAllianceName = useMemo(() => - state.flippedTeams ? state.blueAllianceName : state.redAllianceName, - [state.flippedTeams, state.redAllianceName, state.blueAllianceName] - ); - - const rightAllianceName = useMemo(() => - state.flippedTeams ? state.redAllianceName : state.blueAllianceName, - [state.flippedTeams, state.redAllianceName, state.blueAllianceName] + const blueTeam = useMemo(() => + teams.find(t => t.id === state.blueTeamId), + [teams, state.blueTeamId] ); + // โ”€โ”€โ”€ Flip-aware Derived Values โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ const leftIsRed = !state.flippedTeams; - const rightIsRed = state.flippedTeams; - - // Parse time string to seconds - const parseTimeToSeconds = (timeStr: string): number => { - const [minutes, seconds] = timeStr.split(':').map(Number); - return (minutes || 0) * 60 + (seconds || 0); - }; + const rightIsRed = !!state.flippedTeams; + + const leftTeam = state.flippedTeams ? blueTeam : redTeam; + const rightTeam = state.flippedTeams ? redTeam : blueTeam; + const leftOPR = state.flippedTeams ? sortedBlueOPR : sortedRedOPR; + const rightOPR = state.flippedTeams ? sortedRedOPR : sortedBlueOPR; + const leftOPRChanged = state.flippedTeams ? oprChanged.blue : oprChanged.red; + const rightOPRChanged = state.flippedTeams ? oprChanged.red : oprChanged.blue; + const leftAnimatingScore = state.flippedTeams ? animatingScores.blue : animatingScores.red; + const rightAnimatingScore = state.flippedTeams ? animatingScores.red : animatingScores.blue; + const leftScoreChanged = state.flippedTeams ? scoreChanged.blue : scoreChanged.red; + const rightScoreChanged = state.flippedTeams ? scoreChanged.red : scoreChanged.blue; + const leftAllianceName = state.flippedTeams ? state.blueAllianceName : state.redAllianceName; + const rightAllianceName = state.flippedTeams ? state.redAllianceName : state.blueAllianceName; + + // โ”€โ”€โ”€ RP / Shift Computed Values โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + const autoWinner: 'red' | 'blue' | null = useMemo(() => + autoFuelRed > autoFuelBlue ? 'red' : + autoFuelBlue > autoFuelRed ? 'blue' : null, + [autoFuelRed, autoFuelBlue] + ); - // Determine timer color based on time + const totalSeconds = parseTimeToSeconds(state.matchTime); + + const redHubActive = useMemo(() => { + if (totalSeconds === 0) return false; + if (totalSeconds > 140) return true; // AUTO + if (totalSeconds > 130) return true; // TRANSITION + if (totalSeconds > 105) return autoWinner === 'blue'; // SHIFT 1 + if (totalSeconds > 80) return autoWinner !== 'blue'; // SHIFT 2 + if (totalSeconds > 55) return autoWinner === 'blue'; // SHIFT 3 + if (totalSeconds > 30) return autoWinner !== 'blue'; // SHIFT 4 + return true; // END GAME + }, [totalSeconds, autoWinner]); + + const blueHubActive = useMemo(() => { + if (totalSeconds === 0) return false; + if (totalSeconds > 140) return true; + if (totalSeconds > 130) return true; + if (totalSeconds > 105) return autoWinner !== 'blue'; // SHIFT 1 + if (totalSeconds > 80) return autoWinner === 'blue'; // SHIFT 2 + if (totalSeconds > 55) return autoWinner !== 'blue'; // SHIFT 3 + if (totalSeconds > 30) return autoWinner === 'blue'; // SHIFT 4 + return true; + }, [totalSeconds, autoWinner]); + + const redEnergized = totalFuelRed >= 360; + const redSupercharged = totalFuelRed >= 500; + const redTraversal = towerPointsRed >= 25; + const blueEnergized = totalFuelBlue >= 360; + const blueSupercharged = totalFuelBlue >= 500; + const blueTraversal = towerPointsBlue >= 25; + + const leftHubActive = state.flippedTeams ? blueHubActive : redHubActive; + const rightHubActive = state.flippedTeams ? redHubActive : blueHubActive; + const leftEnergized = state.flippedTeams ? blueEnergized : redEnergized; + const leftSupercharged = state.flippedTeams ? blueSupercharged : redSupercharged; + const leftTraversal = state.flippedTeams ? blueTraversal : redTraversal; + const rightEnergized = state.flippedTeams ? redEnergized : blueEnergized; + const rightSupercharged = state.flippedTeams ? redSupercharged : blueSupercharged; + const rightTraversal = state.flippedTeams ? redTraversal : blueTraversal; + + // โ”€โ”€โ”€ Timer Color โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ const getTimerColor = useMemo(() => { - const totalSeconds = parseTimeToSeconds(state.matchTime); if (totalSeconds === 0) return 'text-red-500'; if (totalSeconds <= 30) return 'text-yellow-400'; - if (totalSeconds <= 150 && totalSeconds > 135) return 'text-green-400'; // Auto period + if (totalSeconds <= 160 && totalSeconds > 140) return 'text-green-400'; return 'text-white'; - }, [state.matchTime]); + }, [totalSeconds]); - // Load teams data + // Load teams useEffect(() => { loadTeams().then(setTeams); }, []); + // Entrance animation useEffect(() => { - // Entrance animation with delay - const timer = setTimeout(() => { - setIsVisible(true); - }, 100); - + const timer = setTimeout(() => setIsVisible(true), 100); return () => clearTimeout(timer); }, []); + // Auto banner useEffect(() => { - // Autonomous period animation - const totalSeconds = parseTimeToSeconds(state.matchTime); - if (totalSeconds <= 150 && totalSeconds > 135) { // 2:30 to 2:15 - setShowAutonomous(true); - } else { - setShowAutonomous(false); - } + const secs = parseTimeToSeconds(state.matchTime); + setShowAutonomous(secs <= 160 && secs > 145); }, [state.matchTime]); + // Game state polling useEffect(() => { - // Read GameState.txt for debugging - if (state.gameFileLocation) { - const readGameState = async () => { - try { - const response = await fetch(`/api/read-file?path=${encodeURIComponent(state.gameFileLocation + '/GameState.txt')}`, { - cache: 'no-store', - headers: { 'Cache-Control': 'no-cache' } - }); - if (response.ok) { - const text = await response.text(); - const trimmedState = text.trim(); - setGameState(trimmedState); - // Show/hide timer based on game state - setShowTimer(trimmedState !== 'FINISHED'); - - // Start/stop differential tracking based on game state - if (trimmedState !== 'FINISHED' && !pointsDifferentialTracker.isActive()) { - pointsDifferentialTracker.startLogging(state.gameFileLocation); - } else if (trimmedState === 'FINISHED' && pointsDifferentialTracker.isActive()) { - pointsDifferentialTracker.stopLogging(); - } - } - } catch { - setGameState('Error reading GameState.txt'); - // If we can't read the game state, show the timer by default - setShowTimer(true); - } - }; - - readGameState(); - const interval = setInterval(readGameState, 100); - return () => clearInterval(interval); - } else { - // If no game file location, show timer by default + if (!state.gameFileLocation) { setShowTimer(true); setGameState(''); + return; } + + const readGameState = async () => { + try { + const response = await fetch( + `/api/read-file?path=${encodeURIComponent(state.gameFileLocation + '/GameState.txt')}`, + { cache: 'no-store', headers: { 'Cache-Control': 'no-cache' } } + ); + if (response.ok) { + const trimmedState = (await response.text()).trim(); + setGameState(trimmedState); + setShowTimer(trimmedState !== 'FINISHED'); + + if (trimmedState !== 'FINISHED' && !pointsDifferentialTracker.isActive()) { + pointsDifferentialTracker.startLogging(state.gameFileLocation); + } else if (trimmedState === 'FINISHED' && pointsDifferentialTracker.isActive()) { + pointsDifferentialTracker.stopLogging(); + setOverlayState({ differentialData: pointsDifferentialTracker.getData() }); + } + } + } catch { + setGameState('Error reading GameState.txt'); + setShowTimer(true); + } + }; + + readGameState(); + const interval = setInterval(readGameState, 100); + return () => clearInterval(interval); }, [state.gameFileLocation]); + // Score animation useEffect(() => { - // Score change animation if (prevScores.red !== state.redScore) { setScoreChanged(prev => ({ ...prev, red: true })); setAnimatingScores(prev => ({ ...prev, red: state.redScore })); setTimeout(() => setScoreChanged(prev => ({ ...prev, red: false })), 500); } - if (prevScores.blue !== state.blueScore) { setScoreChanged(prev => ({ ...prev, blue: true })); setAnimatingScores(prev => ({ ...prev, blue: state.blueScore })); setTimeout(() => setScoreChanged(prev => ({ ...prev, blue: false })), 500); } - - // Log points differential when scores change pointsDifferentialTracker.logPoint(state.redScore, state.blueScore, state.matchTime, gameState); - setPrevScores({ red: state.redScore, blue: state.blueScore }); }, [state.redScore, state.blueScore, state.matchTime, gameState]); + // OPR animation useEffect(() => { - // Update animating OPR to match current state setAnimatingOPR({ red: state.redOPR, blue: state.blueOPR }); - - // OPR change animation + const animateOPRChanges = (alliance: 'red' | 'blue') => { const currentOPR = state[alliance === 'red' ? 'redOPR' : 'blueOPR']; const prevOPRData = prevOPR[alliance]; const changedPlayers = new Set(); - + currentOPR.forEach((player, index) => { const prevPlayer = prevOPRData[index]; if (prevPlayer && prevPlayer.username === player.username && prevPlayer.score !== player.score) { changedPlayers.add(player.username); - - // Animate from old score to new score + const startScore = prevPlayer.score; const endScore = player.score; const startTime = Date.now(); const duration = 200; - + const animateScore = () => { const elapsed = Date.now() - startTime; const progress = Math.min(elapsed / duration, 1); - const easeOutCubic = 1 - Math.pow(1 - progress, 3); const currentScore = Math.round(startScore + (endScore - startScore) * easeOutCubic); - + setAnimatingOPR(prev => ({ ...prev, - [alliance]: prev[alliance].map(p => + [alliance]: prev[alliance].map(p => p.username === player.username ? { ...p, score: currentScore } : p ) })); - + if (progress < 1) { requestAnimationFrame(animateScore); } else { setOprChanged(prev => ({ ...prev, - [alliance]: new Set([...prev[alliance]].filter(name => name !== player.username)) + [alliance]: new Set([...prev[alliance]].filter(n => n !== player.username)) })); } }; - + requestAnimationFrame(animateScore); } }); - + if (changedPlayers.size > 0) { setOprChanged(prev => ({ ...prev, @@ -268,65 +269,97 @@ export default function MatchView({ state, currentTime }: MatchViewProps) { })); } }; - + animateOPRChanges('red'); animateOPRChanges('blue'); - setPrevOPR({ red: state.redOPR, blue: state.blueOPR }); }, [state.redOPR, state.blueOPR]); + // RP file polling (REBUILT only) + useEffect(() => { + if (!isRebuilt) return; + if (!state.gameFileLocation || !state.gameFileLocation.trim()) return; + + const read = (file: string) => + fetch(`/api/read-file?path=${encodeURIComponent(state.gameFileLocation + '/' + file)}`, { + cache: 'no-store', headers: { 'Cache-Control': 'no-cache' } + }) + .then(r => r.ok ? r.text() : '0') + .then(t => parseFloat(t.trim()) || 0) + .catch(() => 0); + + const readRPFiles = async () => { + const [autoFR, autoFB, teleFR, teleFB, endR, endB, autoL1R, autoL1B] = await Promise.all([ + read('Auto_Fuel_R.txt'), + read('Auto_Fuel_B.txt'), + read('Tele_Fuel_R.txt'), + read('Tele_Fuel_B.txt'), + read('End_R.txt'), + read('End_B.txt'), + read('Auto_lvl_1_R.txt'), + read('Auto_lvl_1_B.txt'), + ]); + + setAutoFuelRed(autoFR); + setAutoFuelBlue(autoFB); + setTotalFuelRed(autoFR + teleFR); + setTotalFuelBlue(autoFB + teleFB); + setTowerPointsRed(endR + autoL1R * 15); + setTowerPointsBlue(endB + autoL1B * 15); + }; + + readRPFiles(); + const interval = setInterval(readRPFiles, 500); + return () => clearInterval(interval); + }, [isRebuilt, state.gameFileLocation]); + // โ”€โ”€โ”€ Render โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ return ( <>
-
- {state.matchTitle} -
+
{state.matchTitle}
{state.matchNumber && state.matchNumber.trim() && ( -
- {state.matchNumber} -
+
{state.matchNumber}
)}
-
- {currentTime} -
+
{currentTime}
-
-
+ {/* Timer Container */} -
- {/* Autonomous Animation - Above timer */} +
AUTO
-
-
{state.matchTime}
+
+ {state.matchTime} +
- + {/* Score Container */} -
- {/* Left Side Logo */} {leftTeam?.logo && (
{`${leftAllianceName}
)} - - {/* Right Side Logo */} {rightTeam?.logo && (
{`${rightAllianceName}
@@ -366,7 +392,7 @@ export default function MatchView({ state, currentTime }: MatchViewProps) { )} - {/* Logo centered */} + {/* Center Logo */}
- +
+ {/* Left side */}
- {/* Left Alliance OPR */} + {/* Left OPR */}
{leftOPR.map((player, index) => ( -
+
{player.username} {player.score} + } ${leftOPRChanged.has(player.username) ? 'animate-score-text-change' : ''}`}> + {player.score} +
))}
- - {/* Left Score */} -
-
{leftAnimatingScore}
+ + {/* Left Score + Indicators */} +
+ {isRebuilt && ( + + )} +
+
{leftAnimatingScore}
+
- - {/* Center logo space */} + + {/* Center space */}
- + {/* Right side */}
- {/* Right Score */} -
-
{rightAnimatingScore}
+ {/* Right Score + Indicators */} +
+ {isRebuilt && ( + + )} +
+
{rightAnimatingScore}
+
- - {/* Right Alliance OPR */} + + {/* Right OPR */}
{rightOPR.map((player, index) => ( -
+
{player.score} + } ${rightOPRChanged.has(player.username) ? 'animate-score-text-change' : ''}`}> + {player.score} + {player.username}
))}
+
@@ -445,79 +491,31 @@ export default function MatchView({ state, currentTime }: MatchViewProps) { ); diff --git a/src/app/overlay/components/Results.tsx b/src/app/overlay/components/Results.tsx index 019fe73..2c9d62b 100644 --- a/src/app/overlay/components/Results.tsx +++ b/src/app/overlay/components/Results.tsx @@ -63,13 +63,15 @@ export default function Results({ state }: ResultsProps) { useEffect(() => { - // Show immediately, no delay setIsVisible(true); - - // Get differential data from tracker - setDifferentialData(pointsDifferentialTracker.getData()); }, []); + useEffect(() => { + if (state.differentialData && state.differentialData.length > 0) { + setDifferentialData(state.differentialData); + } + }, [state.differentialData]); + useEffect(() => { // Read files in background after render if (state.gameFileLocation) {