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:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user