generated from nhcarrigan/template
feat: add in-game sound effects and browser notifications (#27)
Adds Web Audio API sound effects and browser notifications for key game events: achievement unlocked, quest completed, quest failed, boss defeated, prestige, transcendence, and apotheosis. Both features are toggled via profile settings, with notification permission requested on first enable.
This commit is contained in:
@@ -44,6 +44,8 @@ const parseProfileSettings = (raw: unknown): ProfileSettings => {
|
||||
? (parsedNumberFormat as ProfileSettings["numberFormat"])
|
||||
: "suffix";
|
||||
return {
|
||||
enableNotifications: rawObject.enableNotifications === true,
|
||||
enableSounds: rawObject.enableSounds === true,
|
||||
numberFormat: numberFormat,
|
||||
showAchievementsUnlocked: rawObject.showAchievementsUnlocked !== false,
|
||||
showAdventurersRecruited: rawObject.showAdventurersRecruited !== false,
|
||||
@@ -203,6 +205,8 @@ profileRouter.put("/", authMiddleware, async(context) => {
|
||||
? (parsedNumberFormat as ProfileSettings["numberFormat"])
|
||||
: "suffix";
|
||||
const profileSettings: ProfileSettings = {
|
||||
enableNotifications: body.profileSettings.enableNotifications ?? false,
|
||||
enableSounds: body.profileSettings.enableSounds ?? false,
|
||||
numberFormat: numberFormat,
|
||||
showAchievementsUnlocked: body.profileSettings.showAchievementsUnlocked ?? true,
|
||||
showAdventurersRecruited: body.profileSettings.showAdventurersRecruited ?? true,
|
||||
|
||||
@@ -245,6 +245,17 @@ const howToPlay = [
|
||||
+ " progress is permanent and survives all resets.",
|
||||
title: "📖 Story",
|
||||
},
|
||||
{
|
||||
body:
|
||||
"Enable sound effects and browser notifications in your profile settings"
|
||||
+ " (click your character name in the top bar). Sound effects play when"
|
||||
+ " you defeat a boss, complete or fail a quest, unlock an achievement,"
|
||||
+ " prestige, transcend, or achieve apotheosis. Browser notifications"
|
||||
+ " alert you to the same events even when the game tab is in the"
|
||||
+ " background. You will be prompted to grant notification permission"
|
||||
+ " when you first enable them.",
|
||||
title: "🔔 Sounds & Notifications",
|
||||
},
|
||||
];
|
||||
|
||||
const formatDate = (dateString: string): string => {
|
||||
|
||||
@@ -16,6 +16,9 @@ import {
|
||||
import { type ChangeEvent, type JSX, useEffect, useState } from "react";
|
||||
import { updateProfile } from "../../api/client.js";
|
||||
import { useGame } from "../../context/gameContext.js";
|
||||
import {
|
||||
requestNotificationPermission,
|
||||
} from "../../utils/notification.js";
|
||||
|
||||
interface EditProfileModalProperties {
|
||||
readonly onClose: ()=> void;
|
||||
@@ -96,6 +99,8 @@ const EditProfileModal = ({
|
||||
state,
|
||||
numberFormat: currentNumberFormat,
|
||||
setNumberFormat,
|
||||
setEnableSounds,
|
||||
setEnableNotifications,
|
||||
} = useGame();
|
||||
const player = state?.player;
|
||||
|
||||
@@ -157,6 +162,8 @@ const EditProfileModal = ({
|
||||
profileSettings,
|
||||
});
|
||||
setNumberFormat(profileSettings.numberFormat);
|
||||
setEnableSounds(profileSettings.enableSounds);
|
||||
setEnableNotifications(profileSettings.enableNotifications);
|
||||
setSaved(true);
|
||||
setTimeout(onClose, 900);
|
||||
} catch (error_: unknown) {
|
||||
@@ -194,6 +201,30 @@ const EditProfileModal = ({
|
||||
setBio(event.target.value);
|
||||
}
|
||||
|
||||
function handleSoundsToggle(): void {
|
||||
toggleSetting("enableSounds");
|
||||
}
|
||||
|
||||
async function handleNotificationsEnable(): Promise<void> {
|
||||
if (profileSettings.enableNotifications) {
|
||||
toggleSetting("enableNotifications");
|
||||
return;
|
||||
}
|
||||
const granted = await requestNotificationPermission();
|
||||
if (granted) {
|
||||
toggleSetting("enableNotifications");
|
||||
} else {
|
||||
setError(
|
||||
"Browser notification permission was denied."
|
||||
+ " Please enable it in your browser settings.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function handleNotificationsToggle(): void {
|
||||
void handleNotificationsEnable();
|
||||
}
|
||||
|
||||
const isSaveDisabled = saving || characterName.trim() === "";
|
||||
|
||||
let saveLabel = "Save Profile";
|
||||
@@ -348,6 +379,46 @@ const EditProfileModal = ({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="edit-profile-section">
|
||||
<p className="edit-profile-label">{"Sounds & Notifications"}</p>
|
||||
<p className="edit-profile-sublabel">
|
||||
{"Control in-game sound effects and browser notifications."}
|
||||
</p>
|
||||
<button
|
||||
className={`stat-toggle-btn ${
|
||||
profileSettings.enableSounds
|
||||
? "stat-toggle-on"
|
||||
: "stat-toggle-off"
|
||||
}`}
|
||||
onClick={handleSoundsToggle}
|
||||
type="button"
|
||||
>
|
||||
<span>{"🔊 Sound Effects"}</span>
|
||||
<span className="stat-toggle-indicator">
|
||||
{profileSettings.enableSounds
|
||||
? "✓ On"
|
||||
: "Off"}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className={`stat-toggle-btn ${
|
||||
profileSettings.enableNotifications
|
||||
? "stat-toggle-on"
|
||||
: "stat-toggle-off"
|
||||
}`}
|
||||
onClick={handleNotificationsToggle}
|
||||
type="button"
|
||||
>
|
||||
<span>{"🔔 Browser Notifications"}</span>
|
||||
<span className="stat-toggle-indicator">
|
||||
{profileSettings.enableNotifications
|
||||
? "✓ On"
|
||||
: "Off"
|
||||
}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="edit-profile-section">
|
||||
<p className="edit-profile-label">{"Number Format"}</p>
|
||||
<p className="edit-profile-sublabel">
|
||||
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
PRESTIGE_UPGRADES,
|
||||
PRESTIGE_UPGRADE_CATEGORY_LABELS,
|
||||
} from "../../data/prestigeUpgrades.js";
|
||||
import { sendNotification } from "../../utils/notification.js";
|
||||
import { playSound } from "../../utils/sound.js";
|
||||
import type { PrestigeUpgradeCategory } from "@elysium/types";
|
||||
|
||||
const baseThreshold = 1_000_000;
|
||||
@@ -84,6 +86,8 @@ const PrestigePanel = (): JSX.Element => {
|
||||
reload,
|
||||
formatNumber,
|
||||
buyPrestigeUpgrade,
|
||||
enableNotifications,
|
||||
enableSounds,
|
||||
toggleAutoPrestige,
|
||||
} = useGame();
|
||||
const [ isPending, setIsPending ] = useState(false);
|
||||
@@ -124,6 +128,15 @@ const PrestigePanel = (): JSX.Element => {
|
||||
milestoneRunestones: data.milestoneRunestones,
|
||||
runestones: data.runestones,
|
||||
});
|
||||
if (enableSounds) {
|
||||
playSound("prestige");
|
||||
}
|
||||
if (enableNotifications) {
|
||||
sendNotification(
|
||||
"⭐ Prestige!",
|
||||
`You've reached prestige level ${data.newPrestigeCount.toString()}!`,
|
||||
);
|
||||
}
|
||||
await reload();
|
||||
} catch (error_: unknown) {
|
||||
setPrestigeError(
|
||||
|
||||
@@ -58,6 +58,8 @@ import {
|
||||
} from "../engine/tick.js";
|
||||
import { updateChallengeProgress } from "../utils/dailyChallenges.js";
|
||||
import { formatNumber as formatNumberUtil } from "../utils/format.js";
|
||||
import { sendNotification } from "../utils/notification.js";
|
||||
import { playSound } from "../utils/sound.js";
|
||||
|
||||
const autoSaveIntervalMs = 30_000;
|
||||
const autoPrestigeThresholdBase = 1_000_000;
|
||||
@@ -342,6 +344,26 @@ interface GameContextValue {
|
||||
*/
|
||||
setNumberFormat: (format: NumberFormat)=> void;
|
||||
|
||||
/**
|
||||
* Whether in-game sound effects are enabled.
|
||||
*/
|
||||
enableSounds: boolean;
|
||||
|
||||
/**
|
||||
* Whether browser system notifications are enabled.
|
||||
*/
|
||||
enableNotifications: boolean;
|
||||
|
||||
/**
|
||||
* Update the enable-sounds preference.
|
||||
*/
|
||||
setEnableSounds: (enabled: boolean)=> void;
|
||||
|
||||
/**
|
||||
* Update the enable-notifications preference.
|
||||
*/
|
||||
setEnableNotifications: (enabled: boolean)=> void;
|
||||
|
||||
/**
|
||||
* Format a number using the player's chosen notation style.
|
||||
*/
|
||||
@@ -499,11 +521,17 @@ export const GameProvider = ({
|
||||
null,
|
||||
);
|
||||
const [ numberFormat, setNumberFormat ] = useState<NumberFormat>("suffix");
|
||||
const [ enableSounds, setEnableSounds ] = useState(false);
|
||||
const [ enableNotifications, setEnableNotifications ] = useState(false);
|
||||
const enableSoundsReference = useRef(false);
|
||||
const enableNotificationsReference = useRef(false);
|
||||
const stateReference = useRef<GameState | null>(null);
|
||||
const lastSaveReference = useRef<number>(Date.now());
|
||||
const isSyncingReference = useRef(false);
|
||||
const rafReference = useRef<number | null>(null);
|
||||
const unlockedAchievementsReference = useRef<Array<Achievement>>([]);
|
||||
const newlyCompletedQuestsCountReference = useRef(0);
|
||||
const newlyFailedQuestsCountReference = useRef(0);
|
||||
const signatureReference = useRef<string | null>(
|
||||
localStorage.getItem("elysium_save_signature"),
|
||||
);
|
||||
@@ -561,7 +589,11 @@ export const GameProvider = ({
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- API response cast
|
||||
const profile = (await response.json()) as {
|
||||
profileSettings?: { numberFormat?: NumberFormat };
|
||||
profileSettings?: {
|
||||
enableNotifications?: boolean;
|
||||
enableSounds?: boolean;
|
||||
numberFormat?: NumberFormat;
|
||||
};
|
||||
};
|
||||
const fmt = profile.profileSettings?.numberFormat;
|
||||
if (
|
||||
@@ -571,10 +603,14 @@ export const GameProvider = ({
|
||||
) {
|
||||
setNumberFormat(fmt);
|
||||
}
|
||||
setEnableSounds(profile.profileSettings?.enableSounds === true);
|
||||
setEnableNotifications(
|
||||
profile.profileSettings?.enableNotifications === true,
|
||||
);
|
||||
}).
|
||||
catch(() => {
|
||||
|
||||
/* Fall back to default "suffix" */
|
||||
/* Fall back to defaults */
|
||||
});
|
||||
} catch (error_: unknown) {
|
||||
setError(
|
||||
@@ -589,6 +625,14 @@ export const GameProvider = ({
|
||||
|
||||
reloadReference.current = reload;
|
||||
|
||||
useEffect(() => {
|
||||
enableSoundsReference.current = enableSounds;
|
||||
}, [ enableSounds ]);
|
||||
|
||||
useEffect(() => {
|
||||
enableNotificationsReference.current = enableNotifications;
|
||||
}, [ enableNotifications ]);
|
||||
|
||||
useEffect(() => {
|
||||
void reload();
|
||||
}, [ reload ]);
|
||||
@@ -904,6 +948,27 @@ export const GameProvider = ({
|
||||
},
|
||||
);
|
||||
|
||||
// Detect newly completed quests
|
||||
newlyCompletedQuestsCountReference.current = next.quests.filter(
|
||||
(q, index) => {
|
||||
return (
|
||||
previous.quests[index]?.status === "active"
|
||||
&& q.status === "completed"
|
||||
);
|
||||
},
|
||||
).length;
|
||||
|
||||
// Detect newly failed quests
|
||||
newlyFailedQuestsCountReference.current = next.quests.filter(
|
||||
(q, index) => {
|
||||
const previousFailedAt = previous.quests[index]?.lastFailedAt;
|
||||
return (
|
||||
q.lastFailedAt !== undefined
|
||||
&& q.lastFailedAt !== previousFailedAt
|
||||
);
|
||||
},
|
||||
).length;
|
||||
|
||||
return next;
|
||||
});
|
||||
|
||||
@@ -911,9 +976,37 @@ export const GameProvider = ({
|
||||
setUnlockedAchievements((previous) => {
|
||||
return [ ...previous, ...unlockedAchievementsReference.current ];
|
||||
});
|
||||
if (enableSoundsReference.current) {
|
||||
playSound("achievement");
|
||||
}
|
||||
if (enableNotificationsReference.current) {
|
||||
for (const achievement of unlockedAchievementsReference.current) {
|
||||
sendNotification("🏆 Achievement Unlocked!", achievement.name);
|
||||
}
|
||||
}
|
||||
unlockedAchievementsReference.current = [];
|
||||
}
|
||||
|
||||
if (newlyCompletedQuestsCountReference.current > 0) {
|
||||
if (enableSoundsReference.current) {
|
||||
playSound("questCompleted");
|
||||
}
|
||||
if (enableNotificationsReference.current) {
|
||||
sendNotification("📜 Quest Complete!", "A quest has been completed.");
|
||||
}
|
||||
newlyCompletedQuestsCountReference.current = 0;
|
||||
}
|
||||
|
||||
if (newlyFailedQuestsCountReference.current > 0) {
|
||||
if (enableSoundsReference.current) {
|
||||
playSound("questFailed");
|
||||
}
|
||||
if (enableNotificationsReference.current) {
|
||||
sendNotification("💀 Quest Failed!", "A quest has failed.");
|
||||
}
|
||||
newlyFailedQuestsCountReference.current = 0;
|
||||
}
|
||||
|
||||
// Auto-save every 30 seconds (skip if a force sync is in-flight to avoid signature collisions)
|
||||
if (Date.now() - lastSaveReference.current >= autoSaveIntervalMs) {
|
||||
lastSaveReference.current = Date.now();
|
||||
@@ -961,6 +1054,12 @@ export const GameProvider = ({
|
||||
isAutoPrestigingReference.current = true;
|
||||
void prestigeApi({}).
|
||||
then(async() => {
|
||||
if (enableSoundsReference.current) {
|
||||
playSound("prestige");
|
||||
}
|
||||
if (enableNotificationsReference.current) {
|
||||
sendNotification("⭐ Prestige!", "You have ascended!");
|
||||
}
|
||||
await reloadReference.current();
|
||||
}).
|
||||
catch(() => {
|
||||
@@ -1004,6 +1103,17 @@ export const GameProvider = ({
|
||||
return applyBossResult(previous, bossId, result);
|
||||
});
|
||||
setBattleResult({ bossName, result });
|
||||
if (result.won) {
|
||||
if (enableSoundsReference.current) {
|
||||
playSound("bossVictory");
|
||||
}
|
||||
if (enableNotificationsReference.current) {
|
||||
sendNotification(
|
||||
"⚔️ Boss Defeated!",
|
||||
`You defeated ${bossName}!`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}).
|
||||
catch(() => {
|
||||
|
||||
@@ -1333,12 +1443,24 @@ export const GameProvider = ({
|
||||
|
||||
const transcend = useCallback(async() => {
|
||||
const result = await transcendApi({});
|
||||
if (enableSoundsReference.current) {
|
||||
playSound("transcendence");
|
||||
}
|
||||
if (enableNotificationsReference.current) {
|
||||
sendNotification("🌌 Transcendence!", "You have transcended reality!");
|
||||
}
|
||||
await reload();
|
||||
return result;
|
||||
}, [ reload ]);
|
||||
|
||||
const apotheosis = useCallback(async() => {
|
||||
const result = await achieveApotheosisApi({});
|
||||
if (enableSoundsReference.current) {
|
||||
playSound("apotheosis");
|
||||
}
|
||||
if (enableNotificationsReference.current) {
|
||||
sendNotification("✨ Apotheosis!", "You have achieved godhood!");
|
||||
}
|
||||
await reload();
|
||||
return result;
|
||||
}, [ reload ]);
|
||||
@@ -1589,6 +1711,14 @@ export const GameProvider = ({
|
||||
return applyBossResult(previous, bossId, result);
|
||||
});
|
||||
setBattleResult({ bossName: boss.name, result: result });
|
||||
if (result.won) {
|
||||
if (enableSoundsReference.current) {
|
||||
playSound("bossVictory");
|
||||
}
|
||||
if (enableNotificationsReference.current) {
|
||||
sendNotification("⚔️ Boss Defeated!", `You defeated ${boss.name}!`);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Silently ignore — server errors shouldn't crash the UI
|
||||
}
|
||||
@@ -1707,6 +1837,8 @@ export const GameProvider = ({
|
||||
dismissLoginBonus,
|
||||
dismissOfflineGold,
|
||||
dismissStoryChapter,
|
||||
enableNotifications,
|
||||
enableSounds,
|
||||
equipItem,
|
||||
error,
|
||||
forceSync,
|
||||
@@ -1725,6 +1857,8 @@ export const GameProvider = ({
|
||||
saveSchemaVersion,
|
||||
schemaOutdated,
|
||||
setActiveCompanion,
|
||||
setEnableNotifications,
|
||||
setEnableSounds,
|
||||
setNumberFormat,
|
||||
startExploration,
|
||||
startQuest,
|
||||
@@ -1758,6 +1892,8 @@ export const GameProvider = ({
|
||||
dismissLoginBonus,
|
||||
dismissOfflineGold,
|
||||
dismissStoryChapter,
|
||||
enableNotifications,
|
||||
enableSounds,
|
||||
equipItem,
|
||||
error,
|
||||
forceSync,
|
||||
@@ -1775,6 +1911,8 @@ export const GameProvider = ({
|
||||
saveSchemaVersion,
|
||||
schemaOutdated,
|
||||
setActiveCompanion,
|
||||
setEnableNotifications,
|
||||
setEnableSounds,
|
||||
setNumberFormat,
|
||||
startExploration,
|
||||
startQuest,
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* @file Browser notification utilities.
|
||||
* @copyright nhcarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
/**
|
||||
* Requests browser notification permission from the user.
|
||||
* @returns Whether permission was granted.
|
||||
*/
|
||||
const requestNotificationPermission = async(): Promise<boolean> => {
|
||||
if (typeof Notification === "undefined") {
|
||||
return false;
|
||||
}
|
||||
if (Notification.permission === "granted") {
|
||||
return true;
|
||||
}
|
||||
if (Notification.permission === "denied") {
|
||||
return false;
|
||||
}
|
||||
const permission = await Notification.requestPermission();
|
||||
return permission === "granted";
|
||||
};
|
||||
|
||||
/**
|
||||
* Sends a browser notification if permission has been granted.
|
||||
* @param title - The notification title text.
|
||||
* @param body - The notification message displayed below the title.
|
||||
*/
|
||||
const sendNotification = (title: string, body: string): void => {
|
||||
if (
|
||||
typeof Notification === "undefined"
|
||||
|| Notification.permission !== "granted"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// eslint-disable-next-line no-new -- Notification constructor has side effects
|
||||
new Notification(title, { body: body, icon: "/favicon.ico" });
|
||||
} catch {
|
||||
// Silently ignore — notifications may fail silently
|
||||
}
|
||||
};
|
||||
|
||||
export { requestNotificationPermission, sendNotification };
|
||||
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* @file Sound effect utilities using the Web Audio API.
|
||||
* @copyright nhcarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
type SoundEvent =
|
||||
| "achievement"
|
||||
| "apotheosis"
|
||||
| "bossVictory"
|
||||
| "prestige"
|
||||
| "questCompleted"
|
||||
| "questFailed"
|
||||
| "transcendence";
|
||||
|
||||
interface SoundPattern {
|
||||
frequencies: Array<number>;
|
||||
gain: number;
|
||||
noteDuration: number;
|
||||
type: OscillatorType;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- intentional constant name
|
||||
const SOUND_PATTERNS: Record<SoundEvent, SoundPattern> = {
|
||||
achievement: {
|
||||
frequencies: [ 523, 659, 784, 1047 ],
|
||||
gain: 0.3,
|
||||
noteDuration: 0.12,
|
||||
type: "triangle",
|
||||
},
|
||||
apotheosis: {
|
||||
frequencies: [ 1047, 880, 784, 659, 523 ],
|
||||
gain: 0.35,
|
||||
noteDuration: 0.25,
|
||||
type: "sine",
|
||||
},
|
||||
bossVictory: {
|
||||
frequencies: [ 523, 784, 1047 ],
|
||||
gain: 0.4,
|
||||
noteDuration: 0.18,
|
||||
type: "square",
|
||||
},
|
||||
prestige: {
|
||||
frequencies: [ 392, 523, 659, 784 ],
|
||||
gain: 0.35,
|
||||
noteDuration: 0.15,
|
||||
type: "sawtooth",
|
||||
},
|
||||
questCompleted: {
|
||||
frequencies: [ 523, 659 ],
|
||||
gain: 0.25,
|
||||
noteDuration: 0.15,
|
||||
type: "sine",
|
||||
},
|
||||
questFailed: {
|
||||
frequencies: [ 392, 330, 261 ],
|
||||
gain: 0.25,
|
||||
noteDuration: 0.18,
|
||||
type: "triangle",
|
||||
},
|
||||
transcendence: {
|
||||
frequencies: [ 261, 329, 392, 523 ],
|
||||
gain: 0.3,
|
||||
noteDuration: 0.3,
|
||||
type: "sine",
|
||||
},
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/init-declarations -- lazily initialised on first use
|
||||
let audioContext: AudioContext | undefined;
|
||||
|
||||
const getAudioContext = (): AudioContext => {
|
||||
if (audioContext === undefined) {
|
||||
audioContext = new AudioContext();
|
||||
}
|
||||
return audioContext;
|
||||
};
|
||||
|
||||
/**
|
||||
* Plays a sound effect for a given game event using the Web Audio API.
|
||||
* @param event - The game event to play a sound for.
|
||||
*/
|
||||
const playSound = (event: SoundEvent): void => {
|
||||
try {
|
||||
const context = getAudioContext();
|
||||
const pattern = SOUND_PATTERNS[event];
|
||||
for (const [ index, frequency ] of pattern.frequencies.entries()) {
|
||||
const oscillator = context.createOscillator();
|
||||
const gainNode = context.createGain();
|
||||
oscillator.connect(gainNode);
|
||||
gainNode.connect(context.destination);
|
||||
oscillator.type = pattern.type;
|
||||
oscillator.frequency.value = frequency;
|
||||
const noteOffset = index * pattern.noteDuration;
|
||||
const startTime = context.currentTime + noteOffset;
|
||||
const endTime = startTime + pattern.noteDuration;
|
||||
gainNode.gain.setValueAtTime(0, startTime);
|
||||
gainNode.gain.linearRampToValueAtTime(pattern.gain, startTime + 0.01);
|
||||
gainNode.gain.linearRampToValueAtTime(0, endTime);
|
||||
oscillator.start(startTime);
|
||||
oscillator.stop(endTime);
|
||||
}
|
||||
} catch {
|
||||
// Silently ignore — audio may not be available in all environments
|
||||
}
|
||||
};
|
||||
|
||||
export type { SoundEvent };
|
||||
export { playSound };
|
||||
@@ -38,10 +38,22 @@ interface ProfileSettings {
|
||||
* Whether this player appears on the public leaderboards.
|
||||
*/
|
||||
showOnLeaderboards: boolean;
|
||||
|
||||
/**
|
||||
* Whether in-game sound effects are enabled.
|
||||
*/
|
||||
enableSounds: boolean;
|
||||
|
||||
/**
|
||||
* Whether browser system notifications are enabled.
|
||||
*/
|
||||
enableNotifications: boolean;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- intentional constant name
|
||||
const DEFAULT_PROFILE_SETTINGS: ProfileSettings = {
|
||||
enableNotifications: false,
|
||||
enableSounds: false,
|
||||
numberFormat: "suffix",
|
||||
showAchievementsUnlocked: true,
|
||||
showAdventurersRecruited: true,
|
||||
|
||||
@@ -20,6 +20,8 @@ describe("DEFAULT_PROFILE_SETTINGS", () => {
|
||||
expect(DEFAULT_PROFILE_SETTINGS.showAdventurersRecruited).toBe(true);
|
||||
expect(DEFAULT_PROFILE_SETTINGS.showAchievementsUnlocked).toBe(true);
|
||||
expect(DEFAULT_PROFILE_SETTINGS.showOnLeaderboards).toBe(true);
|
||||
expect(DEFAULT_PROFILE_SETTINGS.enableSounds).toBe(false);
|
||||
expect(DEFAULT_PROFILE_SETTINGS.enableNotifications).toBe(false);
|
||||
});
|
||||
|
||||
it("defaults numberFormat to suffix", () => {
|
||||
|
||||
Reference in New Issue
Block a user