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 { 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()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
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 { 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
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