Session 17: Sound Toggle (mute/unmute with persistence) Added
This commit is contained in:
+2
-2
@@ -274,8 +274,8 @@ const Achievements = {
|
||||
Game.state.achievements.push(id);
|
||||
UI.showNotification(`Achievement: ${achievement.name}!`, 'achievement');
|
||||
|
||||
// Audio feedback
|
||||
Audio.playAchievement();
|
||||
// Audio feedback (gated by the sound preference)
|
||||
Game.playSound('achievement');
|
||||
|
||||
// Particle effect for achievement unlock
|
||||
const countEl = document.querySelector('#achievements-tab h3');
|
||||
|
||||
+95
-13
@@ -23,6 +23,7 @@ const Game = {
|
||||
// Save Slot System
|
||||
activeSlot: 0, // index of the currently selected save slot (0..SLOTS-1)
|
||||
autoSave: true, // whether the periodic autosave loop is running
|
||||
soundEnabled: true, // whether game sound effects are enabled (persisted preference)
|
||||
|
||||
// Golden Starbloom random event state
|
||||
goldenBlooms: {
|
||||
@@ -56,10 +57,19 @@ const Game = {
|
||||
this.setupEventListeners();
|
||||
this.startGameLoop();
|
||||
this.startAutoSave();
|
||||
Audio.init();
|
||||
// Only initialize the Web Audio context if sound is enabled. This keeps
|
||||
// a muted player's browser from creating an audio context at all.
|
||||
if (this.state.soundEnabled) {
|
||||
Audio.init();
|
||||
}
|
||||
UI.init();
|
||||
Stats.init();
|
||||
PrestigeUI.init();
|
||||
// Reflect the persisted sound preference on the footer button. If the
|
||||
// player muted sound in a previous session, the button must show muted
|
||||
// (green 'on' class is dropped and the icon flips to 🔇) even though the
|
||||
// HTML defaults to enabled.
|
||||
this.syncSoundButton();
|
||||
console.log('🌌 Cosmic Garden initialized!');
|
||||
},
|
||||
|
||||
@@ -177,6 +187,33 @@ const Game = {
|
||||
localStorage.setItem('cosmicGardenAutoSave', String(this.state.autoSave));
|
||||
});
|
||||
|
||||
// Sound toggle button - mirrors the autosave toggle pattern above, but
|
||||
// uses a speaker icon instead of ON/OFF text since the button content
|
||||
// is an emoji. The 'on' class keeps the green accent styling.
|
||||
document.getElementById('sound-btn').addEventListener('click', () => {
|
||||
this.state.soundEnabled = !this.state.soundEnabled;
|
||||
const btn = document.getElementById('sound-btn');
|
||||
if (this.state.soundEnabled) {
|
||||
btn.classList.add('on');
|
||||
btn.textContent = '🔊';
|
||||
// Re-initialize the audio context now that sound is on. Some
|
||||
// browsers require a user gesture to start it, which this click
|
||||
// provides, so Audio.init() will succeed here.
|
||||
if (!Audio.ctx) {
|
||||
Audio.init();
|
||||
} else {
|
||||
Audio.resume();
|
||||
}
|
||||
UI.showNotification('Sound enabled', 'success');
|
||||
} else {
|
||||
btn.classList.remove('on');
|
||||
btn.textContent = '🔇';
|
||||
UI.showNotification('Sound muted', 'info');
|
||||
}
|
||||
// Persist the toggle preference immediately.
|
||||
localStorage.setItem('cosmicGardenSound', String(this.state.soundEnabled));
|
||||
});
|
||||
|
||||
// Tab switching
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
@@ -205,8 +242,8 @@ const Game = {
|
||||
this.addStardust(amount);
|
||||
this.state.totalClicks++;
|
||||
|
||||
// Audio feedback
|
||||
Audio.playClick();
|
||||
// Audio feedback (gated by the sound preference)
|
||||
this.playSound('click');
|
||||
|
||||
// Visual feedback
|
||||
UI.showFloatingText(event.clientX, event.clientY, `+${this.formatNumber(amount)}`);
|
||||
@@ -329,8 +366,8 @@ const Game = {
|
||||
|
||||
UI.showNotification(`Bought ${BUILDINGS[buildingId].name}!`, 'success');
|
||||
|
||||
// Audio feedback
|
||||
Audio.playPurchase();
|
||||
// Audio feedback (gated by the sound preference)
|
||||
this.playSound('purchase');
|
||||
|
||||
// Particle effect on building purchase
|
||||
const card = document.getElementById(`building-${buildingId}`);
|
||||
@@ -387,8 +424,8 @@ const Game = {
|
||||
if (count > 0) {
|
||||
UI.showNotification(`Bought ${count}x ${building.name}!`, 'success');
|
||||
|
||||
// Audio feedback
|
||||
Audio.playPurchase();
|
||||
// Audio feedback (gated by the sound preference)
|
||||
this.playSound('purchase');
|
||||
|
||||
// Particle effect on building purchase
|
||||
const card = document.getElementById(`building-${buildingId}`);
|
||||
@@ -499,7 +536,7 @@ const Game = {
|
||||
|
||||
if (bought.length > 0) {
|
||||
UI.showNotification(`Bought ${bought.length} upgrade(s)!`, 'success');
|
||||
Audio.playPurchase();
|
||||
this.playSound('purchase');
|
||||
UI.update();
|
||||
}
|
||||
|
||||
@@ -546,7 +583,7 @@ const Game = {
|
||||
|
||||
if (bought.length > 0) {
|
||||
UI.showNotification(`Bought ${bought.length} prestige upgrade(s)!`, 'achievement');
|
||||
Audio.playPurchase();
|
||||
this.playSound('purchase');
|
||||
UI.update();
|
||||
}
|
||||
|
||||
@@ -566,6 +603,40 @@ const Game = {
|
||||
return this.state.goldenBlooms.active ? Math.ceil(this.state.goldenBlooms.timer) : 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* Play a game sound effect, gated by the player's sound preference.
|
||||
* This is the single entry point for all audio so that muting sound
|
||||
* (via the footer toggle) reliably silences every effect in the game —
|
||||
* clicks, purchases, achievements, prestige and golden blooms alike.
|
||||
*/
|
||||
playSound(effect) {
|
||||
if (!this.state.soundEnabled) return;
|
||||
switch (effect) {
|
||||
case 'click': Audio.playClick(); break;
|
||||
case 'purchase': Audio.playPurchase(); break;
|
||||
case 'achievement': Audio.playAchievement(); break;
|
||||
case 'prestige': Audio.playPrestige(); break;
|
||||
case 'goldenBloom': Audio.playGoldenBloom(); break;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Reflect the current sound preference on the footer toggle button's visual
|
||||
* state (green accent class + speaker icon). Call this after loading a save
|
||||
* so a muted player sees a muted button even though the HTML defaults to ON.
|
||||
*/
|
||||
syncSoundButton() {
|
||||
const btn = document.getElementById('sound-btn');
|
||||
if (!btn) return;
|
||||
if (this.state.soundEnabled) {
|
||||
btn.classList.add('on');
|
||||
btn.textContent = '🔊';
|
||||
} else {
|
||||
btn.classList.remove('on');
|
||||
btn.textContent = '🔇';
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Award a Golden Starbloom bonus and apply the production multiplier.
|
||||
* Called when the player clicks a visible bloom. Returns the amount earned.
|
||||
@@ -588,7 +659,7 @@ const Game = {
|
||||
this.state.productionBoostUntil = Date.now() + 30000;
|
||||
|
||||
UI.showNotification('⭐ Golden Starbloom! +' + this.formatNumber(bonus) + ' ✨', 'achievement');
|
||||
Audio.playGoldenBloom();
|
||||
this.playSound('goldenBloom');
|
||||
Particles.createGoldenEffect(x, y);
|
||||
UI.update();
|
||||
return bonus;
|
||||
@@ -716,7 +787,7 @@ const Game = {
|
||||
if (persistedSlot !== null) {
|
||||
this.state.activeSlot = parseInt(persistedSlot, 10) || 0;
|
||||
}
|
||||
|
||||
|
||||
// Calculate offline progress
|
||||
const offlineTime = (Date.now() - this.state.lastSaveTime) / 1000;
|
||||
if (offlineTime > 60) {
|
||||
@@ -741,6 +812,15 @@ const Game = {
|
||||
console.error('Failed to load save:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore the persisted sound preference over whatever the slot save
|
||||
// contained. This is stored separately from the per-slot save so a muted
|
||||
// player's choice survives reloading even with no save on disk, and
|
||||
// switching slots never re-enables sound unexpectedly.
|
||||
const persistedSound = localStorage.getItem('cosmicGardenSound');
|
||||
if (persistedSound !== null) {
|
||||
this.state.soundEnabled = persistedSound === 'true';
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -896,7 +976,8 @@ const Game = {
|
||||
// from a previous slot cannot leak into a fresh slot.
|
||||
productionBoostUntil: 0,
|
||||
activeSlot: slot,
|
||||
autoSave: this.state.autoSave
|
||||
autoSave: this.state.autoSave,
|
||||
soundEnabled: this.state.soundEnabled
|
||||
};
|
||||
UI.showNotification(
|
||||
`Empty Slot ${slot + 1} - new game started!`, 'success'
|
||||
@@ -978,7 +1059,8 @@ const Game = {
|
||||
prestigeUpgrades: [],
|
||||
goldenBlooms: { active: false, timer: 0, lastSpawn: Date.now(), totalClaimed: 0 },
|
||||
activeSlot: this.state.activeSlot,
|
||||
autoSave: this.state.autoSave
|
||||
autoSave: this.state.autoSave,
|
||||
soundEnabled: this.state.soundEnabled
|
||||
};
|
||||
UI.update();
|
||||
UI.showNotification('Game reset!', 'warning');
|
||||
|
||||
+3
-3
@@ -54,8 +54,8 @@ const PRESTIGE = {
|
||||
prestigeUpgrades: Game.state.prestigeUpgrades || []
|
||||
};
|
||||
|
||||
// Audio feedback
|
||||
Audio.playPrestige();
|
||||
// Audio feedback (gated by the sound preference)
|
||||
Game.playSound('prestige');
|
||||
|
||||
// Particle effect for prestige
|
||||
const btn = document.getElementById('prestige-btn');
|
||||
@@ -88,7 +88,7 @@ const PRESTIGE = {
|
||||
Game.state.prestigeUpgrades.push(upgradeId);
|
||||
|
||||
UI.showNotification(`Unlocked: ${upgrade.name}!`, 'achievement');
|
||||
Audio.playPurchase();
|
||||
Game.playSound('purchase');
|
||||
UI.update();
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user