feat: implement smooth train exit with fade-out through right tunnel

- Allow train position to exceed 100% so entire train can exit before route change
- Change route completion threshold from 100% to 135% (locomotive + 5 cars at 7% spacing)
- Add fade-out effect for locomotive and train cars between 92-97% position
- Symmetric with fade-in effect (3-8%) used for left tunnel entrance
- Prevents jarring snap-back when route changes mid-train

The train now smoothly fades out as it enters the right tunnel, with the route
only changing after the entire train (including all passenger cars) has fully
exited through the tunnel.

🤖 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:02:41 -05:00
parent 93cb070ca5
commit 01766944f0
2 changed files with 60 additions and 13 deletions

View File

@@ -318,14 +318,25 @@ export function SteamTrainJourney({ momentum, trainPosition, pressure, elapsedTi
// Calculate position for this car (behind the locomotive)
const carPosition = Math.max(0, trainPosition - (carIndex + 1) * carSpacing)
// Calculate opacity: fade in as car emerges from tunnel (after 3% of track)
const fadeStartPosition = 3
const fadeEndPosition = 8
let opacity = 0
if (carPosition > fadeEndPosition) {
opacity = 1
} else if (carPosition > fadeStartPosition) {
opacity = (carPosition - fadeStartPosition) / (fadeEndPosition - fadeStartPosition)
// Calculate opacity: fade in at left tunnel (3-8%), fade out at right tunnel (92-97%)
const fadeInStart = 3
const fadeInEnd = 8
const fadeOutStart = 92
const fadeOutEnd = 97
let opacity = 1 // Default to fully visible
// Fade in from left tunnel
if (carPosition <= fadeInStart) {
opacity = 0
} else if (carPosition < fadeInEnd) {
opacity = (carPosition - fadeInStart) / (fadeInEnd - fadeInStart)
}
// Fade out into right tunnel
else if (carPosition >= fadeOutEnd) {
opacity = 0
} else if (carPosition > fadeOutStart) {
opacity = 1 - ((carPosition - fadeOutStart) / (fadeOutEnd - fadeOutStart))
}
return {
@@ -336,6 +347,29 @@ export function SteamTrainJourney({ momentum, trainPosition, pressure, elapsedTi
})
}, [trainPosition, trackGenerator, maxCars, carSpacing])
// Calculate locomotive opacity (fade in/out through tunnels)
const locomotiveOpacity = useMemo(() => {
const fadeInStart = 3
const fadeInEnd = 8
const fadeOutStart = 92
const fadeOutEnd = 97
// Fade in from left tunnel
if (trainPosition <= fadeInStart) {
return 0
} else if (trainPosition < fadeInEnd) {
return (trainPosition - fadeInStart) / (fadeInEnd - fadeInStart)
}
// Fade out into right tunnel
else if (trainPosition >= fadeOutEnd) {
return 0
} else if (trainPosition > fadeOutStart) {
return 1 - ((trainPosition - fadeOutStart) / (fadeOutEnd - fadeOutStart))
}
return 1 // Default to fully visible
}, [trainPosition])
// Memoize filtered passenger lists to avoid recalculating on every render
const boardedPassengers = useMemo(() =>
state.passengers.filter(p => p.isBoarded && !p.isDelivered),
@@ -846,7 +880,14 @@ export function SteamTrainJourney({ momentum, trainPosition, pressure, elapsedTi
})}
{/* Locomotive - rendered last so it appears on top */}
<g data-component="locomotive-group" transform={`translate(${trainTransform.x}, ${trainTransform.y}) rotate(${trainTransform.rotation}) scale(-1, 1)`}>
<g
data-component="locomotive-group"
transform={`translate(${trainTransform.x}, ${trainTransform.y}) rotate(${trainTransform.rotation}) scale(-1, 1)`}
opacity={locomotiveOpacity}
style={{
transition: 'opacity 0.5s ease-in'
}}
>
{/* Train locomotive */}
<text
data-element="train-locomotive"

View File

@@ -83,8 +83,9 @@ export function useSteamJourney() {
const speed = newMomentum * SPEED_MULTIPLIER
// Update train position (accumulate, never go backward)
// Allow position to go past 100% so entire train (including cars) can exit tunnel
const positionDelta = (speed * deltaTime) / 1000
const trainPosition = Math.min(100, state.trainPosition + positionDelta)
const trainPosition = state.trainPosition + positionDelta
// Calculate pressure (0-150 PSI) - based on momentum as percentage of max
const maxMomentum = 100 // Theoretical max momentum
@@ -130,9 +131,14 @@ export function useSteamJourney() {
})
})
// Check for route completion (train reaches 100%)
// Auto-advance to next route for infinite play
if (trainPosition >= 100 && state.trainPosition < 100) {
// Check for route completion (entire train exits tunnel)
// With 5 cars at 7% spacing, last car is at trainPosition - 35%
// So wait until trainPosition >= 135% for entire train to exit
const MAX_CARS = 5
const CAR_SPACING = 7
const ENTIRE_TRAIN_EXIT_THRESHOLD = 100 + (MAX_CARS * CAR_SPACING) // 135%
if (trainPosition >= ENTIRE_TRAIN_EXIT_THRESHOLD && state.trainPosition < ENTIRE_TRAIN_EXIT_THRESHOLD) {
// Play celebration whistle
playSound('train_whistle', 0.6)
setTimeout(() => {