Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fab490ffea | ||
|
|
8b4dacdc98 | ||
|
|
28fc0a14be | ||
|
|
fffaf1df1d | ||
|
|
09df96922e | ||
|
|
0add9e4ef1 | ||
|
|
3eb85d7d72 | ||
|
|
eb3700a57d |
28
CHANGELOG.md
28
CHANGELOG.md
@@ -1,3 +1,31 @@
|
||||
## [4.67.3](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.67.2...v4.67.3) (2025-10-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **complement-race:** resolve infinite render loop in useTrackManagement ([8b4dacd](https://github.com/antialias/soroban-abacus-flashcards/commit/8b4dacdc98cc8cb2a503b31698430ad7ffb6ef8e))
|
||||
|
||||
## [4.67.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.67.1...v4.67.2) (2025-10-23)
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **complement-race:** increase train position update frequency to 60fps ([fffaf1d](https://github.com/antialias/soroban-abacus-flashcards/commit/fffaf1df1d4d55c811bf634c957691e3564470d6))
|
||||
|
||||
## [4.67.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.67.0...v4.67.1) (2025-10-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **complement-race:** fix react-spring interpolation TypeScript errors ([0add9e4](https://github.com/antialias/soroban-abacus-flashcards/commit/0add9e4ef1d69e4e92ffe279cce09c68efa43714))
|
||||
|
||||
## [4.67.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.66.2...v4.67.0) (2025-10-22)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **complement-race:** add react-spring animations to ghost trains for smooth movement ([eb3700a](https://github.com/antialias/soroban-abacus-flashcards/commit/eb3700a57d035a142c64b60d5d1b21181d21b69f))
|
||||
|
||||
## [4.66.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.66.1...v4.66.2) (2025-10-22)
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useSpring, useSprings, animated, to } from '@react-spring/web'
|
||||
import { useMemo, useRef } from 'react'
|
||||
import type { PlayerState } from '@/arcade-games/complement-race/types'
|
||||
import type { RailroadTrackGenerator } from '../../lib/RailroadTrackGenerator'
|
||||
@@ -56,8 +57,8 @@ export function GhostTrain({
|
||||
}: GhostTrainProps) {
|
||||
const ghostRef = useRef<SVGGElement>(null)
|
||||
|
||||
// Calculate ghost train locomotive transform and opacity
|
||||
const locomotiveTransform = useMemo<CarTransform | null>(() => {
|
||||
// Calculate target transform for locomotive (used by spring animation)
|
||||
const locomotiveTarget = useMemo<CarTransform | null>(() => {
|
||||
if (!pathRef.current) {
|
||||
return null
|
||||
}
|
||||
@@ -82,8 +83,17 @@ export function GhostTrain({
|
||||
}
|
||||
}, [trainPosition, localTrainCarPositions, pathRef])
|
||||
|
||||
// Calculate ghost train car transforms (each car behind locomotive)
|
||||
const carTransforms = useMemo<CarTransform[]>(() => {
|
||||
// Animated spring for smooth locomotive movement
|
||||
const locomotiveSpring = useSpring({
|
||||
x: locomotiveTarget?.x ?? 0,
|
||||
y: locomotiveTarget?.y ?? 0,
|
||||
rotation: locomotiveTarget?.rotation ?? 0,
|
||||
opacity: locomotiveTarget?.opacity ?? 1,
|
||||
config: { tension: 280, friction: 60 }, // Smooth but responsive
|
||||
})
|
||||
|
||||
// Calculate target transforms for cars (used by spring animations)
|
||||
const carTargets = useMemo<CarTransform[]>(() => {
|
||||
if (!pathRef.current) {
|
||||
return []
|
||||
}
|
||||
@@ -115,20 +125,32 @@ export function GhostTrain({
|
||||
return cars
|
||||
}, [trainPosition, maxCars, carSpacing, localTrainCarPositions, pathRef])
|
||||
|
||||
// Animated springs for smooth car movement (useSprings for multiple cars)
|
||||
const carSprings = useSprings(
|
||||
carTargets.length,
|
||||
carTargets.map((target) => ({
|
||||
x: target.x,
|
||||
y: target.y,
|
||||
rotation: target.rotation,
|
||||
opacity: target.opacity,
|
||||
config: { tension: 280, friction: 60 },
|
||||
}))
|
||||
)
|
||||
|
||||
// Don't render if position data isn't ready
|
||||
if (!locomotiveTransform) {
|
||||
if (!locomotiveTarget) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<g ref={ghostRef} data-component="ghost-train" data-player-id={player.id}>
|
||||
{/* Ghost locomotive */}
|
||||
<g
|
||||
transform={`translate(${locomotiveTransform.x}, ${locomotiveTransform.y}) rotate(${locomotiveTransform.rotation}) scale(-1, 1)`}
|
||||
opacity={locomotiveTransform.opacity}
|
||||
style={{
|
||||
transition: 'opacity 0.3s ease-in-out',
|
||||
}}
|
||||
{/* Ghost locomotive - animated */}
|
||||
<animated.g
|
||||
transform={to(
|
||||
[locomotiveSpring.x, locomotiveSpring.y, locomotiveSpring.rotation],
|
||||
(x, y, rot) => `translate(${x}, ${y}) rotate(${rot}) scale(-1, 1)`
|
||||
)}
|
||||
opacity={locomotiveSpring.opacity}
|
||||
>
|
||||
<text
|
||||
data-element="ghost-locomotive"
|
||||
@@ -179,17 +201,17 @@ export function GhostTrain({
|
||||
>
|
||||
{player.score}
|
||||
</text>
|
||||
</g>
|
||||
</animated.g>
|
||||
|
||||
{/* Ghost cars - each with individual opacity */}
|
||||
{carTransforms.map((car, index) => (
|
||||
<g
|
||||
{/* Ghost cars - each with individual animated opacity and position */}
|
||||
{carSprings.map((spring, index) => (
|
||||
<animated.g
|
||||
key={`car-${index}`}
|
||||
transform={`translate(${car.x}, ${car.y}) rotate(${car.rotation}) scale(-1, 1)`}
|
||||
opacity={car.opacity}
|
||||
style={{
|
||||
transition: 'opacity 0.3s ease-in-out',
|
||||
}}
|
||||
transform={to(
|
||||
[spring.x, spring.y, spring.rotation],
|
||||
(x, y, rot) => `translate(${x}, ${y}) rotate(${rot}) scale(-1, 1)`
|
||||
)}
|
||||
opacity={spring.opacity}
|
||||
>
|
||||
<text
|
||||
data-element={`ghost-car-${index}`}
|
||||
@@ -204,7 +226,7 @@ export function GhostTrain({
|
||||
>
|
||||
🚃
|
||||
</text>
|
||||
</g>
|
||||
</animated.g>
|
||||
))}
|
||||
</g>
|
||||
)
|
||||
|
||||
@@ -34,7 +34,7 @@ const MOMENTUM_DECAY_RATES = {
|
||||
|
||||
const MOMENTUM_GAIN_PER_CORRECT = 15 // Momentum added for each correct answer
|
||||
const SPEED_MULTIPLIER = 0.15 // Convert momentum to speed (% per second at momentum=100)
|
||||
const UPDATE_INTERVAL = 50 // Update every 50ms (~20 fps)
|
||||
const UPDATE_INTERVAL = 16 // Update every 16ms (~60 fps for smooth animation)
|
||||
const GAME_DURATION = 60000 // 60 seconds in milliseconds
|
||||
|
||||
export function useSteamJourney() {
|
||||
|
||||
@@ -104,7 +104,9 @@ export function useTrackManagement({
|
||||
setDisplayPassengers(passengers)
|
||||
}
|
||||
// Otherwise, keep displaying old passengers until train resets
|
||||
}, [passengers, displayPassengers, trainPosition, currentRoute])
|
||||
// Note: displayPassengers is intentionally NOT in deps to avoid infinite loop
|
||||
// (it's used for comparison, but we don't need to re-run when it changes)
|
||||
}, [passengers, trainPosition, currentRoute])
|
||||
|
||||
// Generate ties and rails when path is ready
|
||||
useEffect(() => {
|
||||
|
||||
@@ -365,7 +365,7 @@ export function ComplementRaceProvider({ children }: { children: ReactNode }) {
|
||||
const MOMENTUM_GAIN_PER_CORRECT = 15
|
||||
const MOMENTUM_LOSS_PER_WRONG = 10
|
||||
const SPEED_MULTIPLIER = 0.15 // momentum * 0.15 = % per second
|
||||
const UPDATE_INTERVAL = 50 // 50ms = ~20fps
|
||||
const UPDATE_INTERVAL = 16 // 16ms = ~60fps for smooth animation
|
||||
|
||||
// Transform multiplayer state to look like single-player state
|
||||
const compatibleState = useMemo((): CompatibleGameState => {
|
||||
@@ -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(() => {
|
||||
@@ -636,7 +641,7 @@ export function ComplementRaceProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
console.log('[POS_BROADCAST] Starting position broadcast interval')
|
||||
|
||||
// Send position update every 100ms for smoother ghost trains (reads from refs to avoid restarting interval)
|
||||
// Send position update every 16ms (~60fps) for smoother ghost trains (reads from refs to avoid restarting interval)
|
||||
const interval = setInterval(() => {
|
||||
const currentPos = clientPositionRef.current
|
||||
broadcastCountRef.current++
|
||||
@@ -659,7 +664,7 @@ export function ComplementRaceProvider({ children }: { children: ReactNode }) {
|
||||
userId: viewerId || '',
|
||||
data: { position: currentPos },
|
||||
} as ComplementRaceMove)
|
||||
}, 100)
|
||||
}, 16)
|
||||
|
||||
return () => {
|
||||
console.log(`[POS_BROADCAST] Stopping interval (sent ${broadcastCountRef.current} updates)`)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "soroban-monorepo",
|
||||
"version": "4.66.2",
|
||||
"version": "4.67.3",
|
||||
"private": true,
|
||||
"description": "Beautiful Soroban Flashcard Generator - Monorepo",
|
||||
"workspaces": [
|
||||
|
||||
Reference in New Issue
Block a user