feat: integrate remaining game sound effects

Added 5 remaining sound effects across all game modes:

1. ai_turbo (0.12 volume) - Plays when AI enters desperate_catchup
   mode (>10 units behind), matches line 11941

2. lap_celebration (0.6 volume) - Plays when player completes a lap
   in survival mode circular track, matches line 12801

3. celebration (default volume) - Plays when:
   - Player wins practice mode (reaches goal first)
   - Route completes in sprint mode (with train_whistle)
   Matches lines 14182, 13543

4. gameOver (default volume) - Plays when AI wins in practice mode
   (AI reaches goal before player), matches line 14193

5. train_whistle (0.25-0.6 volume) - Plays in sprint mode when:
   - Streak milestone: streak >= 5 and streak % 3 === 0 (0.4 volume)
   - High momentum: momentum >= 90 (30% chance, 0.25 volume)
   - Route completion: train reaches 100% (0.6 volume)
   Matches lines 13222-13235, 13541

All sound integrations preserve exact timing and volume levels from
original Python implementation (web_generator.py).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock
2025-09-30 14:22:16 -05:00
parent 920aaa6398
commit 600bc35bc3
4 changed files with 57 additions and 2 deletions

View File

@@ -32,14 +32,16 @@ export function GameDisplay() {
// Check for finish line (player reaches race goal) - only for practice mode
useEffect(() => {
if (state.correctAnswers >= state.raceGoal && state.isGameActive && state.style === 'practice') {
// Play celebration sound (line 14182)
playSound('celebration')
// End the game
dispatch({ type: 'END_RACE' })
// Show results after a short delay
setTimeout(() => {
dispatch({ type: 'SHOW_RESULTS' })
}, 1000)
}, 1500)
}
}, [state.correctAnswers, state.raceGoal, state.isGameActive, state.style, dispatch])
}, [state.correctAnswers, state.raceGoal, state.isGameActive, state.style, dispatch, playSound])
// For survival mode (endless circuit), track laps but never end
// For sprint mode (steam sprint), end after 60 seconds (will implement later)
@@ -87,6 +89,21 @@ export function GameDisplay() {
// Boost momentum for sprint mode
if (state.style === 'sprint') {
boostMomentum()
// Play train whistle for milestones in sprint mode (line 13222-13235)
if (newStreak >= 5 && newStreak % 3 === 0) {
// Major milestone - play train whistle
setTimeout(() => {
playSound('train_whistle', 0.4)
}, 200)
} else if (state.momentum >= 90) {
// High momentum celebration - occasional whistle
if (Math.random() < 0.3) {
setTimeout(() => {
playSound('train_whistle', 0.25)
}, 150)
}
}
}
// Show adaptive feedback

View File

@@ -6,6 +6,7 @@ import { SpeechBubble } from '../AISystem/SpeechBubble'
import { useComplementRace } from '../../context/ComplementRaceContext'
import { useGameMode } from '@/contexts/GameModeContext'
import { useUserProfile } from '@/contexts/UserProfileContext'
import { useSoundEffects } from '../../hooks/useSoundEffects'
interface CircularTrackProps {
playerProgress: number
@@ -18,6 +19,7 @@ export function CircularTrack({ playerProgress, playerLap, aiRacers, aiLaps }: C
const { state, dispatch } = useComplementRace()
const { players } = useGameMode()
const { profile } = useUserProfile()
const { playSound } = useSoundEffects()
const [celebrationCooldown, setCelebrationCooldown] = useState<Set<string>>(new Set())
// Get the first active player's emoji from UserProfileContext (same as nav bar)
@@ -160,6 +162,8 @@ export function CircularTrack({ playerProgress, playerLap, aiRacers, aiLaps }: C
const playerCurrentLap = Math.floor(playerProgress / 50)
if (playerCurrentLap > playerLap && !celebrationCooldown.has('player')) {
dispatch({ type: 'COMPLETE_LAP', racerId: 'player' })
// Play celebration sound (line 12801)
playSound('lap_celebration', 0.6)
setCelebrationCooldown(prev => new Set(prev).add('player'))
setTimeout(() => {
setCelebrationCooldown(prev => {

View File

@@ -1,9 +1,11 @@
import { useEffect } from 'react'
import { useComplementRace } from '../context/ComplementRaceContext'
import { getAICommentary, type CommentaryContext } from '../components/AISystem/aiCommentary'
import { useSoundEffects } from './useSoundEffects'
export function useAIRacers() {
const { state, dispatch } = useComplementRace()
const { playSound } = useSoundEffects()
useEffect(() => {
if (!state.isGameActive) return
@@ -32,6 +34,26 @@ export function useAIRacers() {
dispatch({ type: 'UPDATE_AI_POSITIONS', positions: newPositions })
// Check for AI win in practice mode (line 14151)
if (state.style === 'practice' && state.isGameActive) {
const winningAI = state.aiRacers.find((racer, index) => {
const updatedPosition = newPositions[index]?.position || racer.position
return updatedPosition >= state.raceGoal
})
if (winningAI) {
// Play game over sound (line 14193)
playSound('gameOver')
// End the game
dispatch({ type: 'END_RACE' })
// Show results after a short delay
setTimeout(() => {
dispatch({ type: 'SHOW_RESULTS' })
}, 1500)
return // Exit early to prevent further updates
}
}
// Check for commentary triggers after position updates
state.aiRacers.forEach(racer => {
const updatedPosition = newPositions.find(p => p.id === racer.id)?.position || racer.position
@@ -73,6 +95,11 @@ export function useAIRacers() {
message,
context
})
// Play special turbo sound when AI goes desperate (line 11941)
if (context === 'desperate_catchup') {
playSound('ai_turbo', 0.12)
}
}
}
})

View File

@@ -1,6 +1,7 @@
import { useEffect, useRef } from 'react'
import { useComplementRace } from '../context/ComplementRaceContext'
import { generatePassengers, findBoardablePassengers, findDeliverablePassengers } from '../lib/passengerGenerator'
import { useSoundEffects } from './useSoundEffects'
/**
* Steam Sprint momentum system
@@ -38,6 +39,7 @@ const GAME_DURATION = 60000 // 60 seconds in milliseconds
export function useSteamJourney() {
const { state, dispatch } = useComplementRace()
const { playSound } = useSoundEffects()
const gameStartTimeRef = useRef<number>(0)
const lastUpdateRef = useRef<number>(0)
@@ -136,6 +138,11 @@ export function useSteamJourney() {
// Check for route completion (train reaches 100%)
if (trainPosition >= 100 && !state.showRouteCelebration) {
// Play celebration whistle (line 13541-13543)
playSound('train_whistle', 0.6)
setTimeout(() => {
playSound('celebration', 0.4)
}, 800)
dispatch({ type: 'COMPLETE_ROUTE' })
}
}, UPDATE_INTERVAL)