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:
@@ -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"
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
Reference in New Issue
Block a user