Compare commits
6 Commits
abacus-rea
...
v4.68.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dde7ca39cc | ||
|
|
f637ddfdb8 | ||
|
|
128da7f3d2 | ||
|
|
5bd0dadfdf | ||
|
|
755487c42d | ||
|
|
e5b58c844c |
21
CHANGELOG.md
21
CHANGELOG.md
@@ -1,3 +1,24 @@
|
||||
## [4.68.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.68.1...v4.68.2) (2025-10-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **complement-race:** prevent passenger delivery render loop causing thrashing ([f637ddf](https://github.com/antialias/soroban-abacus-flashcards/commit/f637ddfdb8a62c85ebf8f08c35927af9ebcdf0d7))
|
||||
|
||||
## [4.68.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.68.0...v4.68.1) (2025-10-23)
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **complement-race:** increase spring animation responsiveness to reduce lag ([5bd0dad](https://github.com/antialias/soroban-abacus-flashcards/commit/5bd0dadfdf9d6d81d7db4374983e40de00effecb))
|
||||
|
||||
## [4.68.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.67.1...v4.68.0) (2025-10-22)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **complement-race:** add react-spring animations to local train for smooth movement ([e5b58c8](https://github.com/antialias/soroban-abacus-flashcards/commit/e5b58c844cacce84b6118b3219b0d1f86e6f74a2))
|
||||
|
||||
## [4.67.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.67.0...v4.67.1) (2025-10-22)
|
||||
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ export function GhostTrain({
|
||||
y: locomotiveTarget?.y ?? 0,
|
||||
rotation: locomotiveTarget?.rotation ?? 0,
|
||||
opacity: locomotiveTarget?.opacity ?? 1,
|
||||
config: { tension: 280, friction: 60 }, // Smooth but responsive
|
||||
config: { tension: 600, friction: 35 }, // Fast/responsive to match local train
|
||||
})
|
||||
|
||||
// Calculate target transforms for cars (used by spring animations)
|
||||
@@ -133,7 +133,7 @@ export function GhostTrain({
|
||||
y: target.y,
|
||||
rotation: target.rotation,
|
||||
opacity: target.opacity,
|
||||
config: { tension: 280, friction: 60 },
|
||||
config: { tension: 600, friction: 35 }, // Fast/responsive to match local train
|
||||
}))
|
||||
)
|
||||
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
'use client'
|
||||
|
||||
import { memo } from 'react'
|
||||
import { animated, to } from '@react-spring/web'
|
||||
import type { SpringValue } from '@react-spring/web'
|
||||
import type { BoardingAnimation, DisembarkingAnimation } from '../../hooks/usePassengerAnimations'
|
||||
import type { Passenger } from '@/arcade-games/complement-race/types'
|
||||
|
||||
interface TrainCarTransform {
|
||||
x: number
|
||||
y: number
|
||||
rotation: number
|
||||
position: number
|
||||
opacity: number
|
||||
x: SpringValue<number>
|
||||
y: SpringValue<number>
|
||||
rotation: SpringValue<number>
|
||||
position: SpringValue<number>
|
||||
opacity: SpringValue<number>
|
||||
}
|
||||
|
||||
interface TrainTransform {
|
||||
x: number
|
||||
y: number
|
||||
rotation: number
|
||||
x: SpringValue<number>
|
||||
y: SpringValue<number>
|
||||
rotation: SpringValue<number>
|
||||
}
|
||||
|
||||
interface TrainAndCarsProps {
|
||||
@@ -30,7 +32,7 @@ interface TrainAndCarsProps {
|
||||
trainCars: TrainCarTransform[]
|
||||
boardedPassengers: Passenger[]
|
||||
trainTransform: TrainTransform
|
||||
locomotiveOpacity: number
|
||||
locomotiveOpacity: SpringValue<number>
|
||||
playerEmoji: string
|
||||
momentum: number
|
||||
}
|
||||
@@ -72,14 +74,14 @@ export const TrainAndCars = memo(
|
||||
const passenger = boardedPassengers[carIndex]
|
||||
|
||||
return (
|
||||
<g
|
||||
<animated.g
|
||||
key={`train-car-${carIndex}`}
|
||||
data-component="train-car"
|
||||
transform={`translate(${carTransform.x}, ${carTransform.y}) rotate(${carTransform.rotation}) scale(-1, 1)`}
|
||||
transform={to(
|
||||
[carTransform.x, carTransform.y, carTransform.rotation],
|
||||
(x, y, rot) => `translate(${x}, ${y}) rotate(${rot}) scale(-1, 1)`
|
||||
)}
|
||||
opacity={carTransform.opacity}
|
||||
style={{
|
||||
transition: 'opacity 0.5s ease-in',
|
||||
}}
|
||||
>
|
||||
{/* Train car */}
|
||||
<text
|
||||
@@ -114,18 +116,18 @@ export const TrainAndCars = memo(
|
||||
{passenger.avatar}
|
||||
</text>
|
||||
)}
|
||||
</g>
|
||||
</animated.g>
|
||||
)
|
||||
})}
|
||||
|
||||
{/* Locomotive - rendered last so it appears on top */}
|
||||
<g
|
||||
<animated.g
|
||||
data-component="locomotive-group"
|
||||
transform={`translate(${trainTransform.x}, ${trainTransform.y}) rotate(${trainTransform.rotation}) scale(-1, 1)`}
|
||||
transform={to(
|
||||
[trainTransform.x, trainTransform.y, trainTransform.rotation],
|
||||
(x, y, rot) => `translate(${x}, ${y}) rotate(${rot}) scale(-1, 1)`
|
||||
)}
|
||||
opacity={locomotiveOpacity}
|
||||
style={{
|
||||
transition: 'opacity 0.5s ease-in',
|
||||
}}
|
||||
>
|
||||
{/* Train locomotive */}
|
||||
<text
|
||||
@@ -191,7 +193,7 @@ export const TrainAndCars = memo(
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</g>
|
||||
</animated.g>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ export function useSteamJourney() {
|
||||
const routeExitThresholdRef = useRef<number>(107) // Default for 1 car: 100 + 7
|
||||
const missedPassengersRef = useRef<Set<string>>(new Set()) // Track which passengers have been logged as missed
|
||||
const pendingBoardingRef = useRef<Set<string>>(new Set()) // Track passengers with pending boarding requests across frames
|
||||
const pendingDeliveryRef = useRef<Set<string>>(new Set()) // Track passengers with pending delivery requests across frames
|
||||
const previousTrainPositionRef = useRef<number>(0) // Track previous position to detect threshold crossings
|
||||
|
||||
// Initialize game start time
|
||||
@@ -65,19 +66,23 @@ export function useSteamJourney() {
|
||||
}
|
||||
}, [state.currentRoute, state.passengers, state.stations, state.maxConcurrentPassengers])
|
||||
|
||||
// Clean up pendingBoardingRef when passengers are claimed/delivered or route changes
|
||||
// Clean up pendingBoardingRef and pendingDeliveryRef when passengers are claimed/delivered or route changes
|
||||
useEffect(() => {
|
||||
// Remove passengers from pending set if they've been claimed or delivered
|
||||
// Remove passengers from pending sets if they've been claimed or delivered
|
||||
state.passengers.forEach((passenger) => {
|
||||
if (passenger.claimedBy !== null || passenger.deliveredBy !== null) {
|
||||
pendingBoardingRef.current.delete(passenger.id)
|
||||
}
|
||||
if (passenger.deliveredBy !== null) {
|
||||
pendingDeliveryRef.current.delete(passenger.id)
|
||||
}
|
||||
})
|
||||
}, [state.passengers])
|
||||
|
||||
// Clear all pending boarding requests when route changes
|
||||
// Clear all pending boarding and delivery requests when route changes
|
||||
useEffect(() => {
|
||||
pendingBoardingRef.current.clear()
|
||||
pendingDeliveryRef.current.clear()
|
||||
missedPassengersRef.current.clear()
|
||||
previousTrainPositionRef.current = 0 // Reset previous position for new route
|
||||
}, [state.currentRoute])
|
||||
@@ -159,6 +164,9 @@ export function useSteamJourney() {
|
||||
currentBoardedPassengers.forEach((passenger) => {
|
||||
if (!passenger || passenger.deliveredBy !== null || passenger.carIndex === null) return
|
||||
|
||||
// Skip if delivery already dispatched (prevents render loop spam)
|
||||
if (pendingDeliveryRef.current.has(passenger.id)) return
|
||||
|
||||
const station = state.stations.find((s) => s.id === passenger.destinationStationId)
|
||||
if (!station) return
|
||||
|
||||
@@ -172,6 +180,10 @@ export function useSteamJourney() {
|
||||
console.log(
|
||||
`🎯 DELIVERY: ${passenger.name} delivered from Car ${passenger.carIndex} to ${station.emoji} ${station.name} (+${points} pts) (trainPos=${trainPosition.toFixed(1)}, carPos=${carPosition.toFixed(1)}, stationPos=${station.position})`
|
||||
)
|
||||
|
||||
// Mark as pending BEFORE dispatch to prevent duplicate delivery attempts across frames
|
||||
pendingDeliveryRef.current.add(passenger.id)
|
||||
|
||||
dispatch({
|
||||
type: 'DELIVER_PASSENGER',
|
||||
passengerId: passenger.id,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { useSpring, useSprings } from '@react-spring/web'
|
||||
import type { RailroadTrackGenerator } from '../lib/RailroadTrackGenerator'
|
||||
|
||||
interface TrainTransform {
|
||||
@@ -27,22 +28,24 @@ export function useTrainTransforms({
|
||||
maxCars,
|
||||
carSpacing,
|
||||
}: UseTrainTransformsParams) {
|
||||
const [trainTransform, setTrainTransform] = useState<TrainTransform>({
|
||||
x: 50,
|
||||
y: 300,
|
||||
rotation: 0,
|
||||
})
|
||||
|
||||
// Update train position and rotation
|
||||
useEffect(() => {
|
||||
if (pathRef.current) {
|
||||
const transform = trackGenerator.getTrainTransform(pathRef.current, trainPosition)
|
||||
setTrainTransform(transform)
|
||||
// Calculate target locomotive transform
|
||||
const locomotiveTarget = useMemo<TrainTransform>(() => {
|
||||
if (!pathRef.current) {
|
||||
return { x: 50, y: 300, rotation: 0 }
|
||||
}
|
||||
return trackGenerator.getTrainTransform(pathRef.current, trainPosition)
|
||||
}, [trainPosition, trackGenerator, pathRef])
|
||||
|
||||
// Calculate train car transforms (each car follows behind the locomotive)
|
||||
const trainCars = useMemo((): TrainCarTransform[] => {
|
||||
// Animated spring for smooth locomotive movement
|
||||
const trainTransform = useSpring({
|
||||
x: locomotiveTarget.x,
|
||||
y: locomotiveTarget.y,
|
||||
rotation: locomotiveTarget.rotation,
|
||||
config: { tension: 600, friction: 35 }, // Fast/responsive to avoid lag
|
||||
})
|
||||
|
||||
// Calculate target transforms for train cars (each car follows behind the locomotive)
|
||||
const carTargets = useMemo((): TrainCarTransform[] => {
|
||||
if (!pathRef.current) {
|
||||
return Array.from({ length: maxCars }, () => ({
|
||||
x: 0,
|
||||
@@ -86,8 +89,21 @@ export function useTrainTransforms({
|
||||
})
|
||||
}, [trainPosition, trackGenerator, pathRef, maxCars, carSpacing])
|
||||
|
||||
// Calculate locomotive opacity (fade in/out through tunnels)
|
||||
const locomotiveOpacity = useMemo(() => {
|
||||
// Animated springs for smooth car movement
|
||||
const trainCars = useSprings(
|
||||
carTargets.length,
|
||||
carTargets.map((target) => ({
|
||||
x: target.x,
|
||||
y: target.y,
|
||||
rotation: target.rotation,
|
||||
opacity: target.opacity,
|
||||
position: target.position,
|
||||
config: { tension: 600, friction: 35 }, // Fast/responsive to avoid lag
|
||||
}))
|
||||
)
|
||||
|
||||
// Calculate target locomotive opacity (fade in/out through tunnels)
|
||||
const locomotiveOpacityTarget = useMemo(() => {
|
||||
const fadeInStart = 3
|
||||
const fadeInEnd = 8
|
||||
const fadeOutStart = 92
|
||||
@@ -109,9 +125,15 @@ export function useTrainTransforms({
|
||||
return 1 // Default to fully visible
|
||||
}, [trainPosition])
|
||||
|
||||
// Animated spring for smooth locomotive opacity
|
||||
const locomotiveOpacity = useSpring({
|
||||
opacity: locomotiveOpacityTarget,
|
||||
config: { tension: 600, friction: 35 }, // Fast/responsive to avoid lag
|
||||
})
|
||||
|
||||
return {
|
||||
trainTransform,
|
||||
trainCars,
|
||||
locomotiveOpacity,
|
||||
locomotiveOpacity: locomotiveOpacity.opacity,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -486,7 +486,12 @@ export function ComplementRaceProvider({ children }: { children: ReactNode }) {
|
||||
if (multiplayerState.gamePhase !== 'playing') {
|
||||
hasInitializedPositionRef.current = false
|
||||
}
|
||||
}, [multiplayerState.gamePhase, multiplayerState.config.style, multiplayerState.players, localPlayerId])
|
||||
}, [
|
||||
multiplayerState.gamePhase,
|
||||
multiplayerState.config.style,
|
||||
multiplayerState.players,
|
||||
localPlayerId,
|
||||
])
|
||||
|
||||
// Initialize game start time when game becomes active
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "soroban-monorepo",
|
||||
"version": "4.67.1",
|
||||
"version": "4.68.2",
|
||||
"private": true,
|
||||
"description": "Beautiful Soroban Flashcard Generator - Monorepo",
|
||||
"workspaces": [
|
||||
|
||||
Reference in New Issue
Block a user