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:
parent
f30a56d8e7
commit
cb16a19161
BIN
public/click1.mp3
Normal file
BIN
public/click1.mp3
Normal file
Binary file not shown.
BIN
public/click2.mp3
Normal file
BIN
public/click2.mp3
Normal file
Binary file not shown.
BIN
public/click3.mp3
Normal file
BIN
public/click3.mp3
Normal file
Binary file not shown.
BIN
public/levelup.mp3
Normal file
BIN
public/levelup.mp3
Normal file
Binary file not shown.
BIN
public/onPurchase.mp3
Normal file
BIN
public/onPurchase.mp3
Normal file
Binary file not shown.
BIN
public/upgrade.mp3
Normal file
BIN
public/upgrade.mp3
Normal file
Binary file not shown.
BIN
public/upgrade.mp3.bak
Normal file
BIN
public/upgrade.mp3.bak
Normal file
Binary file not shown.
11
src/App.tsx
11
src/App.tsx
@ -3,6 +3,7 @@ import { BuildingButton } from './components/BuildingButton'
|
||||
import { NextBuildingPreview } from './components/NextBuildingPreview'
|
||||
import { ResetButton } from './components/ResetButton'
|
||||
import { useGameStore } from './store/gameStore'
|
||||
import { playClickSound, initAudio } from './utils/soundUtils'
|
||||
import {
|
||||
ChakraProvider,
|
||||
Box,
|
||||
@ -62,7 +63,9 @@ function App() {
|
||||
const handleKeyPress = (e: KeyboardEvent) => {
|
||||
// Only allow clicks after terms are accepted and modal is closed
|
||||
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) => {
|
||||
// Only allow clicks after terms are accepted and modal is closed
|
||||
if (hasStarted) {
|
||||
click()
|
||||
console.log('Mouse clicked');
|
||||
playClickSound(); // Play click sound
|
||||
click();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle starting the game
|
||||
const handleStartGame = () => {
|
||||
if (agreedToTerms) {
|
||||
// Initialize audio on first user interaction
|
||||
initAudio();
|
||||
setHasStarted(true)
|
||||
onClose()
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ import { Box, HStack, Text, Progress, Flex, Divider, Tooltip, Image } from '@cha
|
||||
import { useGameStore } from '../store/gameStore'
|
||||
import logoImg from '../assets/logo.png'
|
||||
import { ResetButton } from './ResetButton'
|
||||
import { SoundToggleButton } from './SoundToggleButton'
|
||||
|
||||
export function ResourceDisplay() {
|
||||
const { points, pointsPerSecond, clickPower, playerLevel } = useGameStore()
|
||||
@ -57,16 +58,19 @@ export function ResourceDisplay() {
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Reset button */}
|
||||
<Box
|
||||
{/* Control buttons */}
|
||||
<Flex
|
||||
position="absolute"
|
||||
right={4}
|
||||
top="50%"
|
||||
transform="translateY(-50%)"
|
||||
zIndex={1}
|
||||
gap={2}
|
||||
align="center"
|
||||
>
|
||||
<SoundToggleButton />
|
||||
<ResetButton />
|
||||
</Box>
|
||||
</Flex>
|
||||
|
||||
{/* Main content - centered */}
|
||||
<Flex
|
||||
|
||||
28
src/components/SoundToggleButton.tsx
Normal file
28
src/components/SoundToggleButton.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
import { create } from 'zustand'
|
||||
import { persist } from 'zustand/middleware'
|
||||
import { playPurchaseSound, playLevelUpSound, playUpgradeSound } from '../utils/soundUtils'
|
||||
|
||||
// Building information interface
|
||||
export interface BuildingInfo {
|
||||
@ -364,7 +365,19 @@ export const useGameStore = create<GameState>()(
|
||||
buyBuilding: (buildingType: BuildingType) => {
|
||||
const state = get()
|
||||
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) {
|
||||
// Play purchase sound
|
||||
playPurchaseSound()
|
||||
|
||||
set((state) => {
|
||||
const newCount = state[buildingType] + 1
|
||||
const level = state[`${buildingType}Level` as keyof GameState] as number
|
||||
@ -383,20 +396,24 @@ export const useGameStore = create<GameState>()(
|
||||
}
|
||||
},
|
||||
|
||||
buyUpgrade: (upgradeType: UpgradeType) => {
|
||||
const state = get()
|
||||
buyUpgrade: (type: UpgradeType) => {
|
||||
const state = get();
|
||||
|
||||
// Handle upgrade purchase
|
||||
if (upgradeType === 'clickPower') {
|
||||
const cost = calculateClickPowerUpgradeCost(state.clickPowerUpgrades)
|
||||
if (type === 'clickPower') {
|
||||
const cost = get().getClickPowerUpgradeCost();
|
||||
|
||||
if (state.points >= cost) {
|
||||
set((state) => ({
|
||||
// Check if we have enough points
|
||||
if (state.points < cost) return;
|
||||
|
||||
// Apply the upgrade
|
||||
set({
|
||||
points: state.points - cost,
|
||||
clickPower: state.clickPower + 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)
|
||||
|
||||
if (state.points >= cost && state[buildingType] > 0) {
|
||||
// Play upgrade sound instead of purchase sound
|
||||
playUpgradeSound()
|
||||
|
||||
set((state) => {
|
||||
const newLevel = (state[`${buildingType}Level` as keyof GameState] as number) + 1
|
||||
const count = state[buildingType]
|
||||
@ -444,9 +464,16 @@ export const useGameStore = create<GameState>()(
|
||||
}
|
||||
|
||||
// Update player level based on points per second
|
||||
const playerLevel = Math.max(1, Math.floor(Math.log10(state.pointsPerSecond) + 1))
|
||||
if (playerLevel !== state.playerLevel) {
|
||||
set({ playerLevel })
|
||||
const currentLevel = state.playerLevel
|
||||
const newLevel = Math.max(1, Math.floor(Math.log10(state.pointsPerSecond) + 1))
|
||||
|
||||
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
192
src/utils/soundUtils.ts
Normal 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();
|
||||
Loading…
x
Reference in New Issue
Block a user