feat: unify toast styles and add quest/milestone toast notifications
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 1m3s
CI / Lint, Build & Test (push) Successful in 1m5s

- Merge .codex-toast and .achievement-toast into a single .game-toast class
- Fix storyToast inner class names and replace <button> wrapper with <div>
- Add QuestCompleteToast and QuestFailedToast components
- Add MilestoneToast for prestige, transcendence, and apotheosis events
- Move shared toast container to gameLayout so all toasts stack in one column
- Wire quest detection in GameContext to store full Quest objects for toast names
- Trigger prestige toast from both auto-prestige and manual prestige panel
This commit is contained in:
2026-03-08 18:47:42 -07:00
committed by Naomi Carrigan
parent 290c06de83
commit f9c925b9fc
10 changed files with 380 additions and 47 deletions
+138 -10
View File
@@ -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<Quest>;
/**
* Remove a quest from the completed toast queue.
*/
dismissCompletedQuest: (id: string)=> void;
/**
* Queue of newly failed quests (for toast notifications).
*/
failedQuestToasts: Array<Quest>;
/**
* 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<Achievement>
>([]);
const [ completedQuestToasts, setCompletedQuestToasts ] = useState<
Array<Quest>
>([]);
const [ failedQuestToasts, setFailedQuestToasts ] = useState<Array<Quest>>(
[],
);
const [ showPrestigeToast, setShowPrestigeToast ] = useState(false);
const [ showTranscendenceToast, setShowTranscendenceToast ] = useState(false);
const [ showApotheosisToast, setShowApotheosisToast ] = useState(false);
const [ lastSavedAt, setLastSavedAt ] = useState<number | null>(null);
const [ isSyncing, setIsSyncing ] = useState(false);
const [ syncError, setSyncError ] = useState<string | null>(null);
@@ -530,8 +595,8 @@ export const GameProvider = ({
const isSyncingReference = useRef(false);
const rafReference = useRef<number | null>(null);
const unlockedAchievementsReference = useRef<Array<Achievement>>([]);
const newlyCompletedQuestsCountReference = useRef(0);
const newlyFailedQuestsCountReference = useRef(0);
const newlyCompletedQuestsReference = useRef<Array<Quest>>([]);
const newlyFailedQuestsReference = useRef<Array<Quest>>([]);
const signatureReference = useRef<string | null>(
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,