refactor: extract usePassengerAnimations hook from SteamTrainJourney

- Create dedicated hook for passenger boarding/disembarking animations
- Move 112 lines of animation logic to usePassengerAnimations.ts
- Export BoardingAnimation and DisembarkingAnimation types
- Reduce SteamTrainJourney.tsx from 1147 to 1023 lines (124 lines removed)
- Preserve all functionality exactly - no behavioral changes

Benefits:
- Better separation of concerns
- Easier to test animation logic independently
- Improved maintainability

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock
2025-10-01 09:30:58 -05:00
parent fad8636763
commit 32abde107c
2 changed files with 173 additions and 135 deletions

View File

@@ -3,6 +3,7 @@
import { useEffect, useRef, useState, useMemo, memo } from 'react'
import { useSpring, animated } from '@react-spring/web'
import { useSteamJourney } from '../../hooks/useSteamJourney'
import { usePassengerAnimations, type BoardingAnimation, type DisembarkingAnimation } from '../../hooks/usePassengerAnimations'
import { useComplementRace } from '../../context/ComplementRaceContext'
import { RailroadTrackGenerator } from '../../lib/RailroadTrackGenerator'
import { PassengerCard } from '../PassengerCard'
@@ -13,25 +14,6 @@ import { useGameMode } from '@/contexts/GameModeContext'
import { useUserProfile } from '@/contexts/UserProfileContext'
import type { Passenger } from '../../lib/gameTypes'
interface BoardingAnimation {
passenger: Passenger
fromX: number
fromY: number
toX: number
toY: number
carIndex: number
startTime: number
}
interface DisembarkingAnimation {
passenger: Passenger
fromX: number
fromY: number
toX: number
toY: number
startTime: number
}
const BoardingPassengerAnimation = memo(({ animation }: { animation: BoardingAnimation }) => {
const spring = useSpring({
from: { x: animation.fromX, y: animation.fromY, opacity: 1 },
@@ -123,9 +105,16 @@ export function SteamTrainJourney({ momentum, trainPosition, pressure, elapsedTi
const [stationPositions, setStationPositions] = useState<Array<{ x: number; y: number }>>([])
const [landmarks, setLandmarks] = useState<Landmark[]>([])
const [landmarkPositions, setLandmarkPositions] = useState<Array<{ x: number; y: number }>>([])
const [boardingAnimations, setBoardingAnimations] = useState<Map<string, BoardingAnimation>>(new Map())
const [disembarkingAnimations, setDisembarkingAnimations] = useState<Map<string, DisembarkingAnimation>>(new Map())
const previousPassengersRef = useRef<Passenger[]>(state.passengers)
// Passenger animations (extracted to hook)
const { boardingAnimations, disembarkingAnimations } = usePassengerAnimations({
passengers: state.passengers,
stations: state.stations,
stationPositions,
trainPosition,
trackGenerator,
pathRef
})
// Generate landmarks when route changes
useEffect(() => {
@@ -246,119 +235,6 @@ export function SteamTrainJourney({ momentum, trainPosition, pressure, elapsedTi
}
}, [trainPosition, trackGenerator])
// Detect passengers boarding/disembarking and start animations (consolidated for performance)
useEffect(() => {
if (!pathRef.current || stationPositions.length === 0) return
const previousPassengers = previousPassengersRef.current
const currentPassengers = state.passengers
// Find newly boarded passengers
const newlyBoarded = currentPassengers.filter(curr => {
const prev = previousPassengers.find(p => p.id === curr.id)
return curr.isBoarded && prev && !prev.isBoarded
})
// Find newly delivered passengers
const newlyDelivered = currentPassengers.filter(curr => {
const prev = previousPassengers.find(p => p.id === curr.id)
return curr.isDelivered && prev && !prev.isDelivered
})
// Start animation for each newly boarded passenger
newlyBoarded.forEach(passenger => {
// Find origin station
const originStation = state.stations.find(s => s.id === passenger.originStationId)
if (!originStation) return
const stationIndex = state.stations.indexOf(originStation)
const stationPos = stationPositions[stationIndex]
if (!stationPos) return
// Find which car this passenger will be in
const boardedPassengers = currentPassengers.filter(p => p.isBoarded && !p.isDelivered)
const carIndex = boardedPassengers.indexOf(passenger)
// Calculate train car position
const carPosition = Math.max(0, trainPosition - (carIndex + 1) * 7) // 7% spacing
const carTransform = trackGenerator.getTrainTransform(pathRef.current!, carPosition)
// Create boarding animation
const animation: BoardingAnimation = {
passenger,
fromX: stationPos.x,
fromY: stationPos.y - 30,
toX: carTransform.x,
toY: carTransform.y,
carIndex,
startTime: Date.now()
}
setBoardingAnimations(prev => {
const next = new Map(prev)
next.set(passenger.id, animation)
return next
})
// Remove animation after 800ms
setTimeout(() => {
setBoardingAnimations(prev => {
const next = new Map(prev)
next.delete(passenger.id)
return next
})
}, 800)
})
// Start animation for each newly delivered passenger
newlyDelivered.forEach(passenger => {
// Find destination station
const destinationStation = state.stations.find(s => s.id === passenger.destinationStationId)
if (!destinationStation) return
const stationIndex = state.stations.indexOf(destinationStation)
const stationPos = stationPositions[stationIndex]
if (!stationPos) return
// Find which car this passenger was in (before delivery)
const prevBoardedPassengers = previousPassengers.filter(p => p.isBoarded && !p.isDelivered)
const carIndex = prevBoardedPassengers.findIndex(p => p.id === passenger.id)
if (carIndex === -1) return
// Calculate train car position at time of disembarking
const carPosition = Math.max(0, trainPosition - (carIndex + 1) * 7) // 7% spacing
const carTransform = trackGenerator.getTrainTransform(pathRef.current!, carPosition)
// Create disembarking animation (from car to station)
const animation: DisembarkingAnimation = {
passenger,
fromX: carTransform.x,
fromY: carTransform.y,
toX: stationPos.x,
toY: stationPos.y - 30,
startTime: Date.now()
}
setDisembarkingAnimations(prev => {
const next = new Map(prev)
next.set(passenger.id, animation)
return next
})
// Remove animation after 800ms
setTimeout(() => {
setDisembarkingAnimations(prev => {
const next = new Map(prev)
next.delete(passenger.id)
return next
})
}, 800)
})
// Update ref
previousPassengersRef.current = currentPassengers
}, [state.passengers, state.stations, stationPositions, trainPosition, trackGenerator, pathRef])
// Calculate train car transforms (each car follows behind the locomotive)
const maxCars = 5 // Maximum passengers per route
const carSpacing = 7 // Percentage of track between cars

View File

@@ -0,0 +1,162 @@
import { useEffect, useRef, useState } from 'react'
import type { Passenger, Station } from '../lib/gameTypes'
import type { RailroadTrackGenerator } from '../lib/RailroadTrackGenerator'
export interface BoardingAnimation {
passenger: Passenger
fromX: number
fromY: number
toX: number
toY: number
carIndex: number
startTime: number
}
export interface DisembarkingAnimation {
passenger: Passenger
fromX: number
fromY: number
toX: number
toY: number
startTime: number
}
interface UsePassengerAnimationsParams {
passengers: Passenger[]
stations: Station[]
stationPositions: Array<{ x: number; y: number }>
trainPosition: number
trackGenerator: RailroadTrackGenerator
pathRef: React.RefObject<SVGPathElement>
}
export function usePassengerAnimations({
passengers,
stations,
stationPositions,
trainPosition,
trackGenerator,
pathRef
}: UsePassengerAnimationsParams) {
const [boardingAnimations, setBoardingAnimations] = useState<Map<string, BoardingAnimation>>(new Map())
const [disembarkingAnimations, setDisembarkingAnimations] = useState<Map<string, DisembarkingAnimation>>(new Map())
const previousPassengersRef = useRef<Passenger[]>(passengers)
// Detect passengers boarding/disembarking and start animations
useEffect(() => {
if (!pathRef.current || stationPositions.length === 0) return
const previousPassengers = previousPassengersRef.current
const currentPassengers = passengers
// Find newly boarded passengers
const newlyBoarded = currentPassengers.filter(curr => {
const prev = previousPassengers.find(p => p.id === curr.id)
return curr.isBoarded && prev && !prev.isBoarded
})
// Find newly delivered passengers
const newlyDelivered = currentPassengers.filter(curr => {
const prev = previousPassengers.find(p => p.id === curr.id)
return curr.isDelivered && prev && !prev.isDelivered
})
// Start animation for each newly boarded passenger
newlyBoarded.forEach(passenger => {
// Find origin station
const originStation = stations.find(s => s.id === passenger.originStationId)
if (!originStation) return
const stationIndex = stations.indexOf(originStation)
const stationPos = stationPositions[stationIndex]
if (!stationPos) return
// Find which car this passenger will be in
const boardedPassengers = currentPassengers.filter(p => p.isBoarded && !p.isDelivered)
const carIndex = boardedPassengers.indexOf(passenger)
// Calculate train car position
const carPosition = Math.max(0, trainPosition - (carIndex + 1) * 7) // 7% spacing
const carTransform = trackGenerator.getTrainTransform(pathRef.current!, carPosition)
// Create boarding animation
const animation: BoardingAnimation = {
passenger,
fromX: stationPos.x,
fromY: stationPos.y - 30,
toX: carTransform.x,
toY: carTransform.y,
carIndex,
startTime: Date.now()
}
setBoardingAnimations(prev => {
const next = new Map(prev)
next.set(passenger.id, animation)
return next
})
// Remove animation after 800ms
setTimeout(() => {
setBoardingAnimations(prev => {
const next = new Map(prev)
next.delete(passenger.id)
return next
})
}, 800)
})
// Start animation for each newly delivered passenger
newlyDelivered.forEach(passenger => {
// Find destination station
const destinationStation = stations.find(s => s.id === passenger.destinationStationId)
if (!destinationStation) return
const stationIndex = stations.indexOf(destinationStation)
const stationPos = stationPositions[stationIndex]
if (!stationPos) return
// Find which car this passenger was in (before delivery)
const prevBoardedPassengers = previousPassengers.filter(p => p.isBoarded && !p.isDelivered)
const carIndex = prevBoardedPassengers.findIndex(p => p.id === passenger.id)
if (carIndex === -1) return
// Calculate train car position at time of disembarking
const carPosition = Math.max(0, trainPosition - (carIndex + 1) * 7) // 7% spacing
const carTransform = trackGenerator.getTrainTransform(pathRef.current!, carPosition)
// Create disembarking animation (from car to station)
const animation: DisembarkingAnimation = {
passenger,
fromX: carTransform.x,
fromY: carTransform.y,
toX: stationPos.x,
toY: stationPos.y - 30,
startTime: Date.now()
}
setDisembarkingAnimations(prev => {
const next = new Map(prev)
next.set(passenger.id, animation)
return next
})
// Remove animation after 800ms
setTimeout(() => {
setDisembarkingAnimations(prev => {
const next = new Map(prev)
next.delete(passenger.id)
return next
})
}, 800)
})
// Update ref
previousPassengersRef.current = currentPassengers
}, [passengers, stations, stationPositions, trainPosition, trackGenerator, pathRef])
return {
boardingAnimations,
disembarkingAnimations
}
}