Session 17: Sound Toggle (mute/unmute with persistence) Added

This commit is contained in:
2026-08-25 02:17:50 +00:00
parent cc1fa6bc4d
commit b9068e2029
5 changed files with 133 additions and 18 deletions
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}