generated from nhcarrigan/template
feat: v1 prototype — core game systems #30
@@ -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