Add audio feedback for user interactions in App and gameStore. Implement click and level-up sounds, and integrate sound toggle in ResourceDisplay for enhanced user experience. Refactor upgrade logic to include sound effects on purchases and upgrades, improving overall gameplay immersion.

This commit is contained in:
billy 2025-03-30 14:24:53 -04:00
parent f30a56d8e7
commit cb16a19161
12 changed files with 278 additions and 20 deletions

BIN
public/click1.mp3 Normal file

Binary file not shown.

BIN
public/click2.mp3 Normal file

Binary file not shown.

BIN
public/click3.mp3 Normal file

Binary file not shown.

BIN
public/levelup.mp3 Normal file

Binary file not shown.

BIN
public/onPurchase.mp3 Normal file

Binary file not shown.

BIN
public/upgrade.mp3 Normal file

Binary file not shown.

BIN
public/upgrade.mp3.bak Normal file

Binary file not shown.

View File

@ -3,6 +3,7 @@ import { BuildingButton } from './components/BuildingButton'
import { NextBuildingPreview } from './components/NextBuildingPreview' import { NextBuildingPreview } from './components/NextBuildingPreview'
import { ResetButton } from './components/ResetButton' import { ResetButton } from './components/ResetButton'
import { useGameStore } from './store/gameStore' import { useGameStore } from './store/gameStore'
import { playClickSound, initAudio } from './utils/soundUtils'
import { import {
ChakraProvider, ChakraProvider,
Box, Box,
@ -62,7 +63,9 @@ function App() {
const handleKeyPress = (e: KeyboardEvent) => { const handleKeyPress = (e: KeyboardEvent) => {
// Only allow clicks after terms are accepted and modal is closed // Only allow clicks after terms are accepted and modal is closed
if (hasStarted) { if (hasStarted) {
click() console.log('Key pressed:', e.key);
playClickSound(); // Play click sound
click();
} }
} }
@ -74,13 +77,17 @@ function App() {
const handleClick = (e: React.MouseEvent) => { const handleClick = (e: React.MouseEvent) => {
// Only allow clicks after terms are accepted and modal is closed // Only allow clicks after terms are accepted and modal is closed
if (hasStarted) { if (hasStarted) {
click() console.log('Mouse clicked');
playClickSound(); // Play click sound
click();
} }
} }
// Handle starting the game // Handle starting the game
const handleStartGame = () => { const handleStartGame = () => {
if (agreedToTerms) { if (agreedToTerms) {
// Initialize audio on first user interaction
initAudio();
setHasStarted(true) setHasStarted(true)
onClose() onClose()
} }

View File

@ -2,6 +2,7 @@ import { Box, HStack, Text, Progress, Flex, Divider, Tooltip, Image } from '@cha
import { useGameStore } from '../store/gameStore' import { useGameStore } from '../store/gameStore'
import logoImg from '../assets/logo.png' import logoImg from '../assets/logo.png'
import { ResetButton } from './ResetButton' import { ResetButton } from './ResetButton'
import { SoundToggleButton } from './SoundToggleButton'
export function ResourceDisplay() { export function ResourceDisplay() {
const { points, pointsPerSecond, clickPower, playerLevel } = useGameStore() const { points, pointsPerSecond, clickPower, playerLevel } = useGameStore()
@ -57,16 +58,19 @@ export function ResourceDisplay() {
/> />
</Box> </Box>
{/* Reset button */} {/* Control buttons */}
<Box <Flex
position="absolute" position="absolute"
right={4} right={4}
top="50%" top="50%"
transform="translateY(-50%)" transform="translateY(-50%)"
zIndex={1} zIndex={1}
gap={2}
align="center"
> >
<SoundToggleButton />
<ResetButton /> <ResetButton />
</Box> </Flex>
{/* Main content - centered */} {/* Main content - centered */}
<Flex <Flex

View File

@ -0,0 +1,28 @@
import { IconButton, Tooltip, useBoolean } from '@chakra-ui/react'
import { toggleSound, isSoundEnabled, initAudio } from '../utils/soundUtils'
import { useEffect, useState } from 'react'
export function SoundToggleButton() {
// Initialize the state with the current sound enabled state
const [soundOn, setSoundOn] = useState(isSoundEnabled())
const handleToggle = () => {
// Initialize audio when toggling (helps with browser autoplay policies)
initAudio();
const newState = toggleSound()
setSoundOn(newState)
}
return (
<Tooltip label={soundOn ? 'Mute sounds' : 'Enable sounds'}>
<IconButton
aria-label="Toggle sound"
icon={<span>{soundOn ? '🔊' : '🔇'}</span>}
onClick={handleToggle}
variant="ghost"
colorScheme="cyan"
size="md"
/>
</Tooltip>
)
}

View File

@ -1,5 +1,6 @@
import { create } from 'zustand' import { create } from 'zustand'
import { persist } from 'zustand/middleware' import { persist } from 'zustand/middleware'
import { playPurchaseSound, playLevelUpSound, playUpgradeSound } from '../utils/soundUtils'
// Building information interface // Building information interface
export interface BuildingInfo { export interface BuildingInfo {
@ -364,7 +365,19 @@ export const useGameStore = create<GameState>()(
buyBuilding: (buildingType: BuildingType) => { buyBuilding: (buildingType: BuildingType) => {
const state = get() const state = get()
const cost = BUILDING_COSTS[buildingType] const cost = BUILDING_COSTS[buildingType]
// Get building info for level requirement check
const info = BUILDING_INFO[buildingType];
// Check if player meets level requirement
if (state.playerLevel < info.levelRequirement) {
return;
}
if (state.points >= cost) { if (state.points >= cost) {
// Play purchase sound
playPurchaseSound()
set((state) => { set((state) => {
const newCount = state[buildingType] + 1 const newCount = state[buildingType] + 1
const level = state[`${buildingType}Level` as keyof GameState] as number const level = state[`${buildingType}Level` as keyof GameState] as number
@ -383,20 +396,24 @@ export const useGameStore = create<GameState>()(
} }
}, },
buyUpgrade: (upgradeType: UpgradeType) => { buyUpgrade: (type: UpgradeType) => {
const state = get() const state = get();
// Handle upgrade purchase if (type === 'clickPower') {
if (upgradeType === 'clickPower') { const cost = get().getClickPowerUpgradeCost();
const cost = calculateClickPowerUpgradeCost(state.clickPowerUpgrades)
if (state.points >= cost) { // Check if we have enough points
set((state) => ({ if (state.points < cost) return;
// Apply the upgrade
set({
points: state.points - cost, points: state.points - cost,
clickPower: state.clickPower + 1, clickPower: state.clickPower + 1,
clickPowerUpgrades: state.clickPowerUpgrades + 1 clickPowerUpgrades: state.clickPowerUpgrades + 1
})) });
}
// Play upgrade sound instead of purchase sound
playUpgradeSound();
} }
}, },
@ -411,6 +428,9 @@ export const useGameStore = create<GameState>()(
const cost = calculateUpgradeCost(buildingType, currentLevel) const cost = calculateUpgradeCost(buildingType, currentLevel)
if (state.points >= cost && state[buildingType] > 0) { if (state.points >= cost && state[buildingType] > 0) {
// Play upgrade sound instead of purchase sound
playUpgradeSound()
set((state) => { set((state) => {
const newLevel = (state[`${buildingType}Level` as keyof GameState] as number) + 1 const newLevel = (state[`${buildingType}Level` as keyof GameState] as number) + 1
const count = state[buildingType] const count = state[buildingType]
@ -444,9 +464,16 @@ export const useGameStore = create<GameState>()(
} }
// Update player level based on points per second // Update player level based on points per second
const playerLevel = Math.max(1, Math.floor(Math.log10(state.pointsPerSecond) + 1)) const currentLevel = state.playerLevel
if (playerLevel !== state.playerLevel) { const newLevel = Math.max(1, Math.floor(Math.log10(state.pointsPerSecond) + 1))
set({ playerLevel })
if (newLevel !== currentLevel) {
// If level has increased, play the level up sound
if (newLevel > currentLevel) {
playLevelUpSound()
}
set({ playerLevel: newLevel })
} }
}, },

192
src/utils/soundUtils.ts Normal file
View File

@ -0,0 +1,192 @@
// Sound effect file paths
const SOUND_FILES = {
CLICK: ['/click1.mp3', '/click2.mp3', '/click3.mp3'],
PURCHASE: '/onPurchase.mp3',
LEVEL_UP: '/levelup.mp3',
UPGRADE: '/upgrade.mp3'
};
// AudioManager singleton for handling all game audio
class AudioManager {
private soundEnabled: boolean = true;
private audioContext: AudioContext | null = null;
private sounds: Map<string, HTMLAudioElement> = new Map();
private lastClickIndex: number = -1;
private initialized: boolean = false;
// Volume levels for different sound types
private readonly VOLUMES = {
CLICK: 0.4,
PURCHASE: 0.5,
LEVEL_UP: 0.7,
UPGRADE: 0.6
};
constructor() {
this.preloadSounds();
}
// Initialize audio context - must be called on user interaction
public init(): void {
console.log('Initializing AudioManager...');
if (this.initialized) {
console.log('AudioManager already initialized');
return;
}
try {
// Create AudioContext if possible (will only work after user interaction)
if (window.AudioContext || (window as any).webkitAudioContext) {
const AudioContextClass = window.AudioContext || (window as any).webkitAudioContext;
this.audioContext = new AudioContextClass();
}
// Play a silent sound to unlock audio on iOS/Safari
const silentSound = new Audio();
silentSound.play().catch(e => console.log('Silent sound failed to play:', e));
// Mark as initialized
this.initialized = true;
console.log('AudioManager initialized successfully');
} catch (error) {
console.error('Failed to initialize AudioManager:', error);
}
}
// Preload all sound effects
private preloadSounds(): void {
try {
// Preload click sounds
SOUND_FILES.CLICK.forEach((path, index) => {
const audio = new Audio(path);
this.sounds.set(`click_${index}`, audio);
audio.load(); // Begin loading
});
// Preload level up sound
const levelUpAudio = new Audio(SOUND_FILES.LEVEL_UP);
this.sounds.set('level_up', levelUpAudio);
levelUpAudio.load();
// Preload purchase sound
const purchaseAudio = new Audio(SOUND_FILES.PURCHASE);
this.sounds.set('purchase', purchaseAudio);
purchaseAudio.load();
// Preload upgrade sound
const upgradeAudio = new Audio(SOUND_FILES.UPGRADE);
this.sounds.set('upgrade', upgradeAudio);
upgradeAudio.load();
console.log("All sounds preloaded");
} catch (error) {
console.error("Error preloading sounds:", error);
}
}
// Play a specific sound file
private playSound(key: string, volume: number): void {
if (!this.soundEnabled) {
console.log('Sound is disabled, not playing', key);
return;
}
// Try to initialize audio if not already done
if (!this.initialized) {
this.init();
}
try {
const sound = this.sounds.get(key);
if (sound) {
console.log(`Playing sound: ${key}`);
// Clone the audio to allow overlapping sounds
const clonedSound = sound.cloneNode() as HTMLAudioElement;
clonedSound.volume = volume;
clonedSound.play().catch(error => {
console.error(`Error playing sound ${key}:`, error);
// If the first attempt failed due to interaction policy,
// try again after a slight delay
if (error.name === 'NotAllowedError') {
console.log('Attempting to play after delay due to browser restrictions');
setTimeout(() => {
const retrySound = new Audio(sound.src);
retrySound.volume = volume;
retrySound.play().catch(e => console.error('Retry failed:', e));
}, 100);
}
});
} else {
console.warn(`Sound not found: ${key}`);
}
} catch (error) {
console.error(`Error playing sound ${key}:`, error);
}
}
// Play a random click sound
public playClickSound(): void {
console.log('Playing click sound...');
// Get a random index that's different from the last one
let index;
do {
index = Math.floor(Math.random() * SOUND_FILES.CLICK.length);
} while (index === this.lastClickIndex && SOUND_FILES.CLICK.length > 1);
this.lastClickIndex = index;
this.playSound(`click_${index}`, this.VOLUMES.CLICK);
}
// Play the purchase sound
public playPurchaseSound(): void {
console.log('Playing purchase sound...');
this.playSound('purchase', this.VOLUMES.PURCHASE);
}
// Play the level up sound
public playLevelUpSound(): void {
console.log('Playing level up sound...');
this.playSound('level_up', this.VOLUMES.LEVEL_UP);
}
// Play the upgrade sound
public playUpgradeSound(): void {
console.log('Playing upgrade sound...');
this.playSound('upgrade', this.VOLUMES.UPGRADE);
}
// Toggle sound on/off
public toggleSound(state?: boolean): boolean {
if (typeof state === 'boolean') {
this.soundEnabled = state;
} else {
this.soundEnabled = !this.soundEnabled;
}
console.log(`Sound is now ${this.soundEnabled ? 'enabled' : 'disabled'}`);
return this.soundEnabled;
}
// Check if sound is enabled
public isSoundEnabled(): boolean {
return this.soundEnabled;
}
}
// Create a singleton instance
const audioManager = new AudioManager();
// Export the functions to use throughout the application
export const initAudio = () => audioManager.init();
export const playClickSound = () => audioManager.playClickSound();
export const playPurchaseSound = () => audioManager.playPurchaseSound();
export const playLevelUpSound = () => audioManager.playLevelUpSound();
export const playUpgradeSound = () => audioManager.playUpgradeSound();
export const toggleSound = (state?: boolean) => audioManager.toggleSound(state);
export const isSoundEnabled = () => audioManager.isSoundEnabled();