refactor: extract RailroadTrackPath component from SteamTrainJourney

Extract track rendering (ties, rails, path, landmarks, stations) into
a separate RailroadTrackPath component for better maintainability.

- Created RailroadTrackPath.tsx (192 lines)
- Reduced SteamTrainJourney.tsx from 687 to 546 lines (-141 lines, 20.5%)
- Preserved all track elements and passenger rendering exactly

🤖 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:55:46 -05:00
parent 05bb035db5
commit d9acc0efea
2 changed files with 210 additions and 154 deletions

View File

@@ -0,0 +1,197 @@
'use client'
import { memo } from 'react'
import type { Station, Passenger } from '../../lib/gameTypes'
import type { Landmark } from '../../lib/landmarks'
interface RailroadTrackPathProps {
tiesAndRails: {
ties: Array<{ x1: number; y1: number; x2: number; y2: number }>
leftRailPoints: string[]
rightRailPoints: string[]
} | null
referencePath: string
pathRef: React.RefObject<SVGPathElement>
landmarkPositions: Array<{ x: number; y: number }>
landmarks: Landmark[]
stationPositions: Array<{ x: number; y: number }>
stations: Station[]
passengers: Passenger[]
boardingAnimations: Map<string, unknown>
disembarkingAnimations: Map<string, unknown>
}
export const RailroadTrackPath = memo(({
tiesAndRails,
referencePath,
pathRef,
landmarkPositions,
landmarks,
stationPositions,
stations,
passengers,
boardingAnimations,
disembarkingAnimations
}: RailroadTrackPathProps) => {
return (
<>
{/* Railroad ties */}
{tiesAndRails?.ties.map((tie, index) => (
<line
key={`tie-${index}`}
x1={tie.x1}
y1={tie.y1}
x2={tie.x2}
y2={tie.y2}
stroke="#654321"
strokeWidth="5"
strokeLinecap="round"
opacity="0.8"
/>
))}
{/* Left rail */}
{tiesAndRails && tiesAndRails.leftRailPoints.length > 1 && (
<polyline
points={tiesAndRails.leftRailPoints.join(' ')}
fill="none"
stroke="#C0C0C0"
strokeWidth="5"
strokeLinecap="round"
/>
)}
{/* Right rail */}
{tiesAndRails && tiesAndRails.rightRailPoints.length > 1 && (
<polyline
points={tiesAndRails.rightRailPoints.join(' ')}
fill="none"
stroke="#C0C0C0"
strokeWidth="5"
strokeLinecap="round"
/>
)}
{/* Reference path (invisible, used for positioning) */}
<path
ref={pathRef}
d={referencePath}
fill="none"
stroke="transparent"
strokeWidth="2"
/>
{/* Landmarks - background scenery */}
{landmarkPositions.map((pos, index) => (
<text
key={`landmark-${index}`}
x={pos.x}
y={pos.y}
textAnchor="middle"
style={{
fontSize: `${(landmarks[index]?.size || 24) * 2.0}px`,
pointerEvents: 'none',
opacity: 0.7,
filter: 'drop-shadow(0 2px 3px rgba(0, 0, 0, 0.2))'
}}
>
{landmarks[index]?.emoji}
</text>
))}
{/* Station markers */}
{stationPositions.map((pos, index) => {
const station = stations[index]
// Find passengers waiting at this station (exclude currently boarding)
const waitingPassengers = passengers.filter(p =>
p.originStationId === station?.id && !p.isBoarded && !p.isDelivered && !boardingAnimations.has(p.id)
)
// Find passengers delivered at this station (exclude currently disembarking)
const deliveredPassengers = passengers.filter(p =>
p.destinationStationId === station?.id && p.isDelivered && !disembarkingAnimations.has(p.id)
)
return (
<g key={`station-${index}`}>
{/* Station platform */}
<circle
cx={pos.x}
cy={pos.y}
r="18"
fill="#8B4513"
stroke="#654321"
strokeWidth="4"
/>
{/* Station icon */}
<text
x={pos.x}
y={pos.y - 40}
textAnchor="middle"
fontSize="48"
style={{ pointerEvents: 'none' }}
>
{station?.icon}
</text>
{/* Station name */}
<text
x={pos.x}
y={pos.y + 50}
textAnchor="middle"
fontSize="20"
fill="#1f2937"
stroke="#f59e0b"
strokeWidth="0.5"
style={{
fontWeight: 900,
pointerEvents: 'none',
fontFamily: '"Comic Sans MS", "Chalkboard SE", "Bradley Hand", cursive',
textShadow: '0 2px 4px rgba(0, 0, 0, 0.2)',
letterSpacing: '0.5px',
paintOrder: 'stroke fill'
}}
>
{station?.name}
</text>
{/* Waiting passengers at this station */}
{waitingPassengers.map((passenger, pIndex) => (
<text
key={`waiting-${passenger.id}`}
x={pos.x + (pIndex - waitingPassengers.length / 2 + 0.5) * 28}
y={pos.y - 30}
textAnchor="middle"
style={{
fontSize: '55px',
pointerEvents: 'none',
filter: passenger.isUrgent ? 'drop-shadow(0 0 8px rgba(245, 158, 11, 0.8))' : 'drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3))'
}}
>
{passenger.avatar}
</text>
))}
{/* Delivered passengers at this station (celebrating) */}
{deliveredPassengers.map((passenger, pIndex) => (
<text
key={`delivered-${passenger.id}`}
x={pos.x + (pIndex - deliveredPassengers.length / 2 + 0.5) * 28}
y={pos.y - 30}
textAnchor="middle"
style={{
fontSize: '55px',
pointerEvents: 'none',
filter: 'drop-shadow(0 0 12px rgba(16, 185, 129, 0.8))',
animation: 'celebrateDelivery 2s ease-out forwards'
}}
>
{passenger.avatar}
</text>
))}
</g>
)
})}
</>
)
})
RailroadTrackPath.displayName = 'RailroadTrackPath'

View File

@@ -14,6 +14,7 @@ import { PressureGauge } from '../PressureGauge'
import { useGameMode } from '@/contexts/GameModeContext'
import { useUserProfile } from '@/contexts/UserProfileContext'
import { TrainTerrainBackground } from './TrainTerrainBackground'
import { RailroadTrackPath } from './RailroadTrackPath'
const BoardingPassengerAnimation = memo(({ animation }: { animation: BoardingAnimation }) => {
const spring = useSpring({
@@ -248,162 +249,20 @@ export function SteamTrainJourney({ momentum, trainPosition, pressure, elapsedTi
groundTextureCircles={groundTextureCircles}
/>
{/* Railroad ties */}
{tiesAndRails?.ties.map((tie, index) => (
<line
key={`tie-${index}`}
x1={tie.x1}
y1={tie.y1}
x2={tie.x2}
y2={tie.y2}
stroke="#654321"
strokeWidth="5"
strokeLinecap="round"
opacity="0.8"
/>
))}
{/* Left rail */}
{tiesAndRails && tiesAndRails.leftRailPoints.length > 1 && (
<polyline
points={tiesAndRails.leftRailPoints.join(' ')}
fill="none"
stroke="#C0C0C0"
strokeWidth="5"
strokeLinecap="round"
/>
)}
{/* Right rail */}
{tiesAndRails && tiesAndRails.rightRailPoints.length > 1 && (
<polyline
points={tiesAndRails.rightRailPoints.join(' ')}
fill="none"
stroke="#C0C0C0"
strokeWidth="5"
strokeLinecap="round"
/>
)}
{/* Reference path (invisible, used for positioning) */}
<path
ref={pathRef}
d={trackData.referencePath}
fill="none"
stroke="transparent"
strokeWidth="2"
{/* Railroad track, landmarks, and stations */}
<RailroadTrackPath
tiesAndRails={tiesAndRails}
referencePath={trackData.referencePath}
pathRef={pathRef}
landmarkPositions={landmarkPositions}
landmarks={landmarks}
stationPositions={stationPositions}
stations={state.stations}
passengers={state.passengers}
boardingAnimations={boardingAnimations}
disembarkingAnimations={disembarkingAnimations}
/>
{/* Landmarks - background scenery */}
{landmarkPositions.map((pos, index) => (
<text
key={`landmark-${index}`}
x={pos.x}
y={pos.y}
textAnchor="middle"
style={{
fontSize: `${(landmarks[index]?.size || 24) * 2.0}px`,
pointerEvents: 'none',
opacity: 0.7,
filter: 'drop-shadow(0 2px 3px rgba(0, 0, 0, 0.2))'
}}
>
{landmarks[index]?.emoji}
</text>
))}
{/* Station markers */}
{stationPositions.map((pos, index) => {
const station = state.stations[index]
// Find passengers waiting at this station (exclude currently boarding)
const waitingPassengers = state.passengers.filter(p =>
p.originStationId === station?.id && !p.isBoarded && !p.isDelivered && !boardingAnimations.has(p.id)
)
// Find passengers delivered at this station (exclude currently disembarking)
const deliveredPassengers = state.passengers.filter(p =>
p.destinationStationId === station?.id && p.isDelivered && !disembarkingAnimations.has(p.id)
)
return (
<g key={`station-${index}`}>
{/* Station platform */}
<circle
cx={pos.x}
cy={pos.y}
r="18"
fill="#8B4513"
stroke="#654321"
strokeWidth="4"
/>
{/* Station icon */}
<text
x={pos.x}
y={pos.y - 40}
textAnchor="middle"
fontSize="48"
style={{ pointerEvents: 'none' }}
>
{station?.icon}
</text>
{/* Station name */}
<text
x={pos.x}
y={pos.y + 50}
textAnchor="middle"
fontSize="20"
fill="#1f2937"
stroke="#f59e0b"
strokeWidth="0.5"
style={{
fontWeight: 900,
pointerEvents: 'none',
fontFamily: '"Comic Sans MS", "Chalkboard SE", "Bradley Hand", cursive',
textShadow: '0 2px 4px rgba(0, 0, 0, 0.2)',
letterSpacing: '0.5px',
paintOrder: 'stroke fill'
}}
>
{station?.name}
</text>
{/* Waiting passengers at this station */}
{waitingPassengers.map((passenger, pIndex) => (
<text
key={`waiting-${passenger.id}`}
x={pos.x + (pIndex - waitingPassengers.length / 2 + 0.5) * 28}
y={pos.y - 30}
textAnchor="middle"
style={{
fontSize: '55px',
pointerEvents: 'none',
filter: passenger.isUrgent ? 'drop-shadow(0 0 8px rgba(245, 158, 11, 0.8))' : 'drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3))'
}}
>
{passenger.avatar}
</text>
))}
{/* Delivered passengers at this station (celebrating) */}
{deliveredPassengers.map((passenger, pIndex) => (
<text
key={`delivered-${passenger.id}`}
x={pos.x + (pIndex - deliveredPassengers.length / 2 + 0.5) * 28}
y={pos.y - 30}
textAnchor="middle"
style={{
fontSize: '55px',
pointerEvents: 'none',
filter: 'drop-shadow(0 0 12px rgba(16, 185, 129, 0.8))',
animation: 'celebrateDelivery 2s ease-out forwards'
}}
>
{passenger.avatar}
</text>
))}
</g>
)
})}
{/* Boarding animations - passengers moving from station to train car */}
{Array.from(boardingAnimations.values()).map(animation => (
<BoardingPassengerAnimation