From f9c925b9fc091e670b90c8b00fbd8c325f266680 Mon Sep 17 00:00:00 2001 From: Hikari Date: Sun, 8 Mar 2026 18:47:42 -0700 Subject: [PATCH] feat: unify toast styles and add quest/milestone toast notifications - Merge .codex-toast and .achievement-toast into a single .game-toast class - Fix storyToast inner class names and replace + ); }; @@ -65,11 +65,11 @@ const StoryToast = (): JSX.Element | null => { return null; } return ( -
+ <> {pendingChapterIds.map((id) => { return ; })} -
+ ); }; diff --git a/apps/web/src/context/gameContext.tsx b/apps/web/src/context/gameContext.tsx index 4ec7ce4..56fca68 100644 --- a/apps/web/src/context/gameContext.tsx +++ b/apps/web/src/context/gameContext.tsx @@ -20,6 +20,7 @@ import { type GameState, type LoginBonusResult, type NumberFormat, + type Quest, type TranscendenceResponse, isStoryChapterUnlocked, } from "@elysium/types"; @@ -334,6 +335,61 @@ interface GameContextValue { */ dismissAchievement: (id: string)=> void; + /** + * Queue of newly completed quests (for toast notifications). + */ + completedQuestToasts: Array; + + /** + * Remove a quest from the completed toast queue. + */ + dismissCompletedQuest: (id: string)=> void; + + /** + * Queue of newly failed quests (for toast notifications). + */ + failedQuestToasts: Array; + + /** + * Remove a quest from the failed toast queue. + */ + dismissFailedQuest: (id: string)=> void; + + /** + * Whether the prestige milestone toast is currently showing. + */ + showPrestigeToast: boolean; + + /** + * Trigger the prestige milestone toast (called from prestigePanel on manual prestige). + */ + triggerPrestigeToast: ()=> void; + + /** + * Dismiss the prestige milestone toast. + */ + dismissPrestigeToast: ()=> void; + + /** + * Whether the transcendence milestone toast is currently showing. + */ + showTranscendenceToast: boolean; + + /** + * Dismiss the transcendence milestone toast. + */ + dismissTranscendenceToast: ()=> void; + + /** + * Whether the apotheosis milestone toast is currently showing. + */ + showApotheosisToast: boolean; + + /** + * Dismiss the apotheosis milestone toast. + */ + dismissApotheosisToast: ()=> void; + /** * The player's chosen number display format. */ @@ -514,6 +570,15 @@ export const GameProvider = ({ const [ unlockedAchievements, setUnlockedAchievements ] = useState< Array >([]); + const [ completedQuestToasts, setCompletedQuestToasts ] = useState< + Array + >([]); + const [ failedQuestToasts, setFailedQuestToasts ] = useState>( + [], + ); + const [ showPrestigeToast, setShowPrestigeToast ] = useState(false); + const [ showTranscendenceToast, setShowTranscendenceToast ] = useState(false); + const [ showApotheosisToast, setShowApotheosisToast ] = useState(false); const [ lastSavedAt, setLastSavedAt ] = useState(null); const [ isSyncing, setIsSyncing ] = useState(false); const [ syncError, setSyncError ] = useState(null); @@ -530,8 +595,8 @@ export const GameProvider = ({ const isSyncingReference = useRef(false); const rafReference = useRef(null); const unlockedAchievementsReference = useRef>([]); - const newlyCompletedQuestsCountReference = useRef(0); - const newlyFailedQuestsCountReference = useRef(0); + const newlyCompletedQuestsReference = useRef>([]); + const newlyFailedQuestsReference = useRef>([]); const signatureReference = useRef( localStorage.getItem("elysium_save_signature"), ); @@ -949,17 +1014,17 @@ export const GameProvider = ({ ); // Detect newly completed quests - newlyCompletedQuestsCountReference.current = next.quests.filter( + newlyCompletedQuestsReference.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( + newlyFailedQuestsReference.current = next.quests.filter( (q, index) => { const previousFailedAt = previous.quests[index]?.lastFailedAt; return ( @@ -967,7 +1032,7 @@ export const GameProvider = ({ && q.lastFailedAt !== previousFailedAt ); }, - ).length; + ); return next; }); @@ -987,24 +1052,30 @@ export const GameProvider = ({ unlockedAchievementsReference.current = []; } - if (newlyCompletedQuestsCountReference.current > 0) { + if (newlyCompletedQuestsReference.current.length > 0) { + setCompletedQuestToasts((previous) => { + return [ ...previous, ...newlyCompletedQuestsReference.current ]; + }); if (enableSoundsReference.current) { playSound("questCompleted"); } if (enableNotificationsReference.current) { sendNotification("📜 Quest Complete!", "A quest has been completed."); } - newlyCompletedQuestsCountReference.current = 0; + newlyCompletedQuestsReference.current = []; } - if (newlyFailedQuestsCountReference.current > 0) { + if (newlyFailedQuestsReference.current.length > 0) { + setFailedQuestToasts((previous) => { + return [ ...previous, ...newlyFailedQuestsReference.current ]; + }); if (enableSoundsReference.current) { playSound("questFailed"); } if (enableNotificationsReference.current) { sendNotification("💀 Quest Failed!", "A quest has failed."); } - newlyFailedQuestsCountReference.current = 0; + newlyFailedQuestsReference.current = []; } // Auto-save every 30 seconds (skip if a force sync is in-flight to avoid signature collisions) @@ -1054,6 +1125,7 @@ export const GameProvider = ({ isAutoPrestigingReference.current = true; void prestigeApi({}). then(async() => { + setShowPrestigeToast(true); if (enableSoundsReference.current) { playSound("prestige"); } @@ -1443,6 +1515,7 @@ export const GameProvider = ({ const transcend = useCallback(async() => { const result = await transcendApi({}); + setShowTranscendenceToast(true); if (enableSoundsReference.current) { playSound("transcendence"); } @@ -1455,6 +1528,7 @@ export const GameProvider = ({ const apotheosis = useCallback(async() => { const result = await achieveApotheosisApi({}); + setShowApotheosisToast(true); if (enableSoundsReference.current) { playSound("apotheosis"); } @@ -1733,6 +1807,38 @@ export const GameProvider = ({ setBattleResult(null); }, []); + const dismissCompletedQuest = useCallback((id: string) => { + setCompletedQuestToasts((previous) => { + return previous.filter((q) => { + return q.id !== id; + }); + }); + }, []); + + const dismissFailedQuest = useCallback((id: string) => { + setFailedQuestToasts((previous) => { + return previous.filter((q) => { + return q.id !== id; + }); + }); + }, []); + + const triggerPrestigeToast = useCallback(() => { + setShowPrestigeToast(true); + }, []); + + const dismissPrestigeToast = useCallback(() => { + setShowPrestigeToast(false); + }, []); + + const dismissTranscendenceToast = useCallback(() => { + setShowTranscendenceToast(false); + }, []); + + const dismissApotheosisToast = useCallback(() => { + setShowApotheosisToast(false); + }, []); + const dismissAchievement = useCallback((id: string) => { setUnlockedAchievements((previous) => { return previous.filter((a) => { @@ -1829,18 +1935,25 @@ export const GameProvider = ({ challengeBoss, collectExploration, completeChapter, + completedQuestToasts, craftRecipe, currentSchemaVersion, dismissAchievement, + dismissApotheosisToast, dismissBattle, dismissCodexEntry, + dismissCompletedQuest, + dismissFailedQuest, dismissLoginBonus, dismissOfflineGold, + dismissPrestigeToast, dismissStoryChapter, + dismissTranscendenceToast, enableNotifications, enableSounds, equipItem, error, + failedQuestToasts, forceSync, formatNumber, handleClick, @@ -1860,6 +1973,9 @@ export const GameProvider = ({ setEnableNotifications, setEnableSounds, setNumberFormat, + showApotheosisToast, + showPrestigeToast, + showTranscendenceToast, startExploration, startQuest, state, @@ -1868,6 +1984,7 @@ export const GameProvider = ({ toggleAutoPrestige, toggleAutoQuest, transcend, + triggerPrestigeToast, unlockedAchievements, unlockedCodexEntryIds, unlockedStoryChapterIds, @@ -1875,6 +1992,8 @@ export const GameProvider = ({ }, [ apotheosis, battleResult, + completedQuestToasts, + failedQuestToasts, formatNumber, buyAdventurer, buyEchoUpgrade, @@ -1887,11 +2006,16 @@ export const GameProvider = ({ craftRecipe, currentSchemaVersion, dismissAchievement, + dismissApotheosisToast, dismissBattle, dismissCodexEntry, + dismissCompletedQuest, + dismissFailedQuest, dismissLoginBonus, dismissOfflineGold, + dismissPrestigeToast, dismissStoryChapter, + dismissTranscendenceToast, enableNotifications, enableSounds, equipItem, @@ -1914,6 +2038,9 @@ export const GameProvider = ({ setEnableNotifications, setEnableSounds, setNumberFormat, + showApotheosisToast, + showPrestigeToast, + showTranscendenceToast, startExploration, startQuest, state, @@ -1922,6 +2049,7 @@ export const GameProvider = ({ toggleAutoPrestige, toggleAutoQuest, transcend, + triggerPrestigeToast, unlockedAchievements, unlockedCodexEntryIds, unlockedStoryChapterIds, diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index 72343e5..125c885 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -1432,20 +1432,6 @@ body { z-index: 200; } -.achievement-toast { - align-items: center; - animation: slide-in-right 0.35s ease-out; - background: var(--colour-surface); - border: 1px solid var(--colour-gold); - border-radius: var(--radius); - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4); - cursor: pointer; - display: flex; - gap: 0.75rem; - max-width: 280px; - padding: 0.75rem 1rem; -} - .toast-icon { font-size: 1.5rem; flex-shrink: 0; @@ -2481,8 +2467,8 @@ body { padding: 0.6rem 0.75rem; } -/* Codex toast — uses a different accent from achievement toast */ -.codex-toast { +/* Unified game toast — essence-coloured border used by all in-game notifications */ +.game-toast { align-items: center; animation: slide-in-right 0.35s ease-out; background: var(--colour-surface);