Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
659464d3b4 | ||
|
|
06cd94b24c | ||
|
|
ada0becee5 | ||
|
|
c5fba5b7dd | ||
|
|
c5bfcf990a | ||
|
|
00dc4b1d06 |
21
CHANGELOG.md
21
CHANGELOG.md
@@ -1,3 +1,24 @@
|
||||
## [4.65.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.65.0...v4.65.1) (2025-10-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **complement-race:** use sendMove with correct parameters for position updates ([06cd94b](https://github.com/antialias/soroban-abacus-flashcards/commit/06cd94b24cdd9dbd36fb5800c9ba7be194f7eed0))
|
||||
|
||||
## [4.65.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.64.2...v4.65.0) (2025-10-22)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **complement-race:** implement position broadcasting for ghost trains ([c5fba5b](https://github.com/antialias/soroban-abacus-flashcards/commit/c5fba5b7dd0f36fd3bbe596409e01b0d3dbd4fbe))
|
||||
|
||||
## [4.64.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.64.1...v4.64.2) (2025-10-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **complement-race:** use individual player positions for ghost trains ([00dc4b1](https://github.com/antialias/soroban-abacus-flashcards/commit/00dc4b1d06a4e1763deb16333a298145cafd9187))
|
||||
|
||||
## [4.64.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.64.0...v4.64.1) (2025-10-22)
|
||||
|
||||
|
||||
|
||||
@@ -47,10 +47,10 @@ export function GhostTrain({ player, trainPosition, trackGenerator, pathRef }: G
|
||||
const hasLoggedRef = useRef(false)
|
||||
useEffect(() => {
|
||||
if (!hasLoggedRef.current && trainTransform.opacity > 0) {
|
||||
console.log('[GhostTrain] rendering:', player.name)
|
||||
console.log('[GhostTrain] rendering:', player.name, 'at position:', trainPosition.toFixed(1))
|
||||
hasLoggedRef.current = true
|
||||
}
|
||||
}, [trainTransform.opacity, player.name])
|
||||
}, [trainTransform.opacity, player.name, trainPosition])
|
||||
|
||||
// Don't render if position data isn't ready
|
||||
if (trainTransform.opacity === 0) {
|
||||
|
||||
@@ -217,7 +217,13 @@ export function SteamTrainJourney({
|
||||
// Log only when otherPlayers count changes
|
||||
useEffect(() => {
|
||||
console.log('[SteamTrainJourney] otherPlayers count:', otherPlayers.length)
|
||||
}, [otherPlayers.length])
|
||||
if (otherPlayers.length > 0) {
|
||||
console.log(
|
||||
'[SteamTrainJourney] ghost positions:',
|
||||
otherPlayers.map((p) => `${p.name}: ${p.position.toFixed(1)}`).join(', ')
|
||||
)
|
||||
}
|
||||
}, [otherPlayers.length, otherPlayers])
|
||||
|
||||
if (!trackData) return null
|
||||
|
||||
@@ -285,7 +291,7 @@ export function SteamTrainJourney({
|
||||
<GhostTrain
|
||||
key={player.id}
|
||||
player={player}
|
||||
trainPosition={trainPosition} // For now, use same position calculation
|
||||
trainPosition={player.position} // Use each player's individual position
|
||||
trackGenerator={trackGenerator}
|
||||
pathRef={pathRef}
|
||||
/>
|
||||
|
||||
@@ -564,6 +564,32 @@ export function ComplementRaceProvider({ children }: { children: ReactNode }) {
|
||||
}
|
||||
}, [multiplayerState.currentRoute, compatibleState.style, multiplayerState.passengers.length])
|
||||
|
||||
// Broadcast position to server for multiplayer ghost trains
|
||||
useEffect(() => {
|
||||
if (!compatibleState.isGameActive || compatibleState.style !== 'sprint' || !localPlayerId) {
|
||||
return
|
||||
}
|
||||
|
||||
// Send position update every 200ms
|
||||
const interval = setInterval(() => {
|
||||
sendMove({
|
||||
type: 'UPDATE_POSITION',
|
||||
playerId: localPlayerId,
|
||||
userId: viewerId || '',
|
||||
data: { position: clientPosition },
|
||||
} as ComplementRaceMove)
|
||||
}, 200)
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [
|
||||
compatibleState.isGameActive,
|
||||
compatibleState.style,
|
||||
clientPosition,
|
||||
localPlayerId,
|
||||
viewerId,
|
||||
sendMove,
|
||||
])
|
||||
|
||||
// Keep lastLogRef for future debugging needs
|
||||
// (removed debug logging)
|
||||
|
||||
|
||||
@@ -97,6 +97,9 @@ export class ComplementRaceValidator
|
||||
case 'UPDATE_INPUT':
|
||||
return this.validateUpdateInput(state, move.playerId, move.data.input)
|
||||
|
||||
case 'UPDATE_POSITION':
|
||||
return this.validateUpdatePosition(state, move.playerId, move.data.position)
|
||||
|
||||
case 'CLAIM_PASSENGER':
|
||||
return this.validateClaimPassenger(
|
||||
state,
|
||||
@@ -397,6 +400,39 @@ export class ComplementRaceValidator
|
||||
return { valid: true, newState }
|
||||
}
|
||||
|
||||
private validateUpdatePosition(
|
||||
state: ComplementRaceState,
|
||||
playerId: string,
|
||||
position: number
|
||||
): ValidationResult {
|
||||
if (state.gamePhase !== 'playing') {
|
||||
return { valid: false, error: 'Game not in playing phase' }
|
||||
}
|
||||
|
||||
const player = state.players[playerId]
|
||||
if (!player) {
|
||||
return { valid: false, error: 'Player not found' }
|
||||
}
|
||||
|
||||
// Validate position is a reasonable number (0-100)
|
||||
if (typeof position !== 'number' || position < 0 || position > 100) {
|
||||
return { valid: false, error: 'Invalid position value' }
|
||||
}
|
||||
|
||||
const newState: ComplementRaceState = {
|
||||
...state,
|
||||
players: {
|
||||
...state.players,
|
||||
[playerId]: {
|
||||
...player,
|
||||
position,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return { valid: true, newState }
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Sprint Mode: Passenger Management
|
||||
// ==========================================================================
|
||||
|
||||
@@ -143,6 +143,7 @@ export type ComplementRaceMove = BaseGameMove &
|
||||
// Playing phase
|
||||
| { type: 'SUBMIT_ANSWER'; data: { answer: number; responseTime: number } }
|
||||
| { type: 'UPDATE_INPUT'; data: { input: string } } // Show "thinking" indicator
|
||||
| { type: 'UPDATE_POSITION'; data: { position: number } } // Sprint mode: sync train position
|
||||
| { type: 'CLAIM_PASSENGER'; data: { passengerId: string; carIndex: number } } // Sprint mode: pickup
|
||||
| { type: 'DELIVER_PASSENGER'; data: { passengerId: string } } // Sprint mode: delivery
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "soroban-monorepo",
|
||||
"version": "4.64.1",
|
||||
"version": "4.65.1",
|
||||
"private": true,
|
||||
"description": "Beautiful Soroban Flashcard Generator - Monorepo",
|
||||
"workspaces": [
|
||||
|
||||
Reference in New Issue
Block a user