Skip to content

Commit

Permalink
feat(audio): audio settings in local storage (#831)
Browse files Browse the repository at this point in the history
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
- Introduced audio settings management with persistent mute state across
sessions.
- Added acceptance tests to validate audio functionality and user
interactions.
	- Enhanced the Mute Button component with improved animation logic.

- **Bug Fixes**
	- Ensured audio settings are correctly restored after a page reload.

- **Tests**
- Expanded test coverage for audio-related functionalities, including
new test cases for the audio store and Mute Button interactions.

- **Documentation**
- Updated feature files to include scenarios for audio control
interactions.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
  • Loading branch information
antoinezanardi authored Sep 8, 2024
1 parent 1357529 commit cd8a2cb
Show file tree
Hide file tree
Showing 13 changed files with 66,623 additions and 65,158 deletions.
12 changes: 12 additions & 0 deletions .run/Audio.run.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Audio" type="cucumber.js" factoryName="Cucumber.js" folderName="Tags">
<option name="myFilePath" value="$PROJECT_DIR$/tests/acceptance" />
<option name="myNameFilter" value="" />
<option name="cucumberJsArguments" value="--config config/cucumber/cucumber.json --parallel 1 --tags @audio" />
<option name="workingDirectory" value="$PROJECT_DIR$" />
<envs>
<env name="NODE_OPTIONS" value="--import tsx/esm" />
</envs>
<method v="2" />
</configuration>
</component>
5 changes: 5 additions & 0 deletions app/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,15 @@

<script lang="ts" setup>
import "reflect-metadata";
import { useAudioStore } from "~/stores/audio/useAudioStore";
import { useRolesStore } from "~/stores/role/useRolesStore";
const rolesStore = useRolesStore();
const audioStore = useAudioStore();
const { setHowlerAudioSettingsFromAudioStoreState } = audioStore;
const { t } = useI18n();
useHead({
Expand All @@ -33,6 +37,7 @@ useHead({
});
void rolesStore.fetchAndSetRoles();
setHowlerAudioSettingsFromAudioStoreState();
</script>

<style lang="scss" scoped>
Expand Down
19 changes: 16 additions & 3 deletions app/components/layouts/default/NavBar/MuteButton/MuteButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,31 @@ const soundLottie = ref<InstanceType<typeof Vue3Lottie> | null>(null);
const tooltipText = computed<string>(() => (isMuted.value ? t("components.MuteButton.unmute") : t("components.MuteButton.mute")));
function onClickFromMuteButton(): void {
function animateSoundLottie(animationDirection: "reverse" | "forward"): void {
if (!soundLottie.value) {
throw createError("Sound Lottie is not initialized");
}
const firstMuteSegmentFrame = 0;
const lastMuteSegmentFrame = 30;
const firstUnmuteSegmentFrame = 60;
const lastUnmuteSegmentFrame = 90;
const animationDirection = isMuted.value ? "reverse" : "forward";
const segment: AnimationSegment = isMuted.value ? [firstUnmuteSegmentFrame, lastUnmuteSegmentFrame] : [firstMuteSegmentFrame, lastMuteSegmentFrame];
const segment: AnimationSegment = animationDirection === "reverse" ? [firstUnmuteSegmentFrame, lastUnmuteSegmentFrame] : [firstMuteSegmentFrame, lastMuteSegmentFrame];
soundLottie.value.setDirection(animationDirection);
soundLottie.value.playSegments([segment], true);
}
function onClickFromMuteButton(): void {
const animationDirection = isMuted.value ? "reverse" : "forward";
animateSoundLottie(animationDirection);
toggleMute();
}
onMounted(() => {
if (isMuted.value) {
const animationDelay = 10;
setTimeout(() => {
animateSoundLottie("forward");
}, animationDelay);
}
});
</script>
7 changes: 7 additions & 0 deletions app/stores/audio/constants/audio.constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
import type { AudioSettings } from "~/stores/audio/types/audio.types";

const DEFAULT_AUDIO_SETTINGS = {
isMuted: false,
} as const satisfies AudioSettings;

const SOUND_EFFECT_NAMES = [
"actor-clear-throat-and-knocks",
"angelic-intervention",
Expand Down Expand Up @@ -53,6 +59,7 @@ const BACKGROUND_AUDIO_NAMES = [
] as const;

export {
DEFAULT_AUDIO_SETTINGS,
SOUND_EFFECT_NAMES,
BACKGROUND_AUDIO_NAMES,
};
5 changes: 5 additions & 0 deletions app/stores/audio/types/audio.types.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import type { TupleToUnion } from "type-fest";
import type { BACKGROUND_AUDIO_NAMES, SOUND_EFFECT_NAMES } from "~/stores/audio/constants/audio.constants";

type AudioSettings = {
isMuted: boolean;
};

type SoundEffectName = TupleToUnion<typeof SOUND_EFFECT_NAMES>;

type BackgroundAudioName = TupleToUnion<typeof BACKGROUND_AUDIO_NAMES>;

export type {
AudioSettings,
SoundEffectName,
BackgroundAudioName,
};
26 changes: 21 additions & 5 deletions app/stores/audio/useAudioStore.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { useLocalStorage } from "@vueuse/core";
import { Howl, Howler } from "howler";
import { draw } from "radash";
import { defineStore } from "pinia";
import type { GamePhaseName } from "~/composables/api/game/types/game-phase/game-phase.types";
import { BACKGROUND_AUDIO_NAMES, SOUND_EFFECT_NAMES } from "~/stores/audio/constants/audio.constants";
import type { BackgroundAudioName, SoundEffectName } from "~/stores/audio/types/audio.types";
import { BACKGROUND_AUDIO_NAMES, DEFAULT_AUDIO_SETTINGS, SOUND_EFFECT_NAMES } from "~/stores/audio/constants/audio.constants";
import type { AudioSettings, BackgroundAudioName, SoundEffectName } from "~/stores/audio/types/audio.types";
import { StoreIds } from "~/stores/enums/store.enum";
import { LocalStorageKeys } from "~/utils/enums/local-storage.enums";

const useAudioStore = defineStore(StoreIds.AUDIO, () => {
const isMuted = ref<boolean>(false);
const audioSettingsFromLocalStorage = useLocalStorage<AudioSettings>(LocalStorageKeys.AUDIO_SETTINGS, DEFAULT_AUDIO_SETTINGS, { mergeDefaults: true });

const isMuted = ref<boolean>(audioSettingsFromLocalStorage.value.isMuted);

const soundEffects = Object.fromEntries(SOUND_EFFECT_NAMES.map(name => [name, createSoundEffect(name)])) as Record<SoundEffectName, Howl>;

Expand All @@ -34,6 +38,10 @@ const useAudioStore = defineStore(StoreIds.AUDIO, () => {
});
}

function setHowlerAudioSettingsFromAudioStoreState(): void {
Howler.mute(isMuted.value);
}

function loadSoundEffects(): void {
Object.values(soundEffects).forEach(soundEffect => soundEffect.load());
}
Expand Down Expand Up @@ -82,24 +90,32 @@ const useAudioStore = defineStore(StoreIds.AUDIO, () => {
playBackgroundAudio(randomGamePhaseBackgroundAudioName);
}

function setMute(isAudioMuted: boolean): void {
isMuted.value = isAudioMuted;
Howler.mute(isAudioMuted);
audioSettingsFromLocalStorage.value.isMuted = isAudioMuted;
}

function toggleMute(): void {
isMuted.value = !isMuted.value;
Howler.mute(isMuted.value);
setMute(!isMuted.value);
}
return {
audioSettingsFromLocalStorage,
isMuted,
soundEffects,
backgroundAudios,
playingBackgroundAudioName,
nightBackgroundAudioNames,
dayBackgroundAudioNames,
setHowlerAudioSettingsFromAudioStoreState,
loadSoundEffects,
loadBackgroundAudios,
loadAllAudios,
playSoundEffect,
fadeOutPlayingBackgroundAudio,
playBackgroundAudio,
playRandomGamePhaseBackgroundAudio,
setMute,
toggleMute,
};
});
Expand Down
1 change: 1 addition & 0 deletions app/utils/enums/local-storage.enums.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
enum LocalStorageKeys {
GAME_OPTIONS = "gameOptions",
AUDIO_SETTINGS = "audioSettings",
}

export { LocalStorageKeys };
32 changes: 32 additions & 0 deletions tests/acceptance/features/audio/features/audio.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
@audio
@shard-3
Feature: 🔊 Audio

Scenario: 🔊 Audio is playing by default in game and can be disabled
Given the user creates a game with the players with name and role
| name | role |
| Antoine | Seer |
| Bob | Werewolf |
| Charlie | Idiot |
| David | Villager |

When the user hovers the button with name "Mute"
Then the tooltip with text "Mute" should be visible

When the user mutes the audio in navigation bar
And the user moves his mouse away
And the user hovers the button with name "Unmute"
Then the tooltip with text "Unmute" should be visible

Scenario: 🔊 Audio settings are saved and restored from local storage
Given the user creates a game with the players with name and role
| name | role |
| Antoine | Seer |
| Bob | Werewolf |
| Charlie | Idiot |
| David | Villager |
And the user mutes the audio in navigation bar

When the user reloads the page
And the user hovers the button with name "Unmute"
Then the tooltip with text "Unmute" should be visible
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { When } from "@cucumber/cucumber";
import { clickOnRoleWithText } from "@tests/acceptance/features/playwright/helpers/roles/playwright-roles.when-steps-helpers";
import type { CustomWorld } from "@tests/acceptance/shared/types/word.types";

When(/^the user mutes the audio in navigation bar$/u, async function(this: CustomWorld): Promise<void> {
await clickOnRoleWithText(this, "button", "Mute", true);
});

When(/^the user unmutes the audio in navigation bar$/u, async function(this: CustomWorld): Promise<void> {
await clickOnRoleWithText(this, "button", "Unmute", true);
});
Loading

0 comments on commit cd8a2cb

Please sign in to comment.