generated from nhcarrigan/template
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 <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:
@@ -41,7 +41,7 @@ const ToastItem = ({
|
|||||||
const crystals = achievement.reward?.crystals;
|
const crystals = achievement.reward?.crystals;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="achievement-toast" onClick={handleClick}>
|
<div className="game-toast" onClick={handleClick}>
|
||||||
<span className="toast-icon">{achievement.icon}</span>
|
<span className="toast-icon">{achievement.icon}</span>
|
||||||
<div className="toast-content">
|
<div className="toast-content">
|
||||||
<span className="toast-label">{"Achievement Unlocked!"}</span>
|
<span className="toast-label">{"Achievement Unlocked!"}</span>
|
||||||
@@ -70,7 +70,7 @@ const AchievementToast = (): JSX.Element | null => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="achievement-toast-container">
|
<>
|
||||||
{pendingAchievements.map((achievement) => {
|
{pendingAchievements.map((achievement) => {
|
||||||
return (
|
return (
|
||||||
<ToastItem
|
<ToastItem
|
||||||
@@ -80,7 +80,7 @@ const AchievementToast = (): JSX.Element | null => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ const CodexToastItem = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="codex-toast" onClick={handleClick}>
|
<div className="game-toast" onClick={handleClick}>
|
||||||
<span className="toast-icon">{"📖"}</span>
|
<span className="toast-icon">{"📖"}</span>
|
||||||
<div className="toast-content">
|
<div className="toast-content">
|
||||||
<span className="toast-label">{"✨ Lore Unlocked!"}</span>
|
<span className="toast-label">{"✨ Lore Unlocked!"}</span>
|
||||||
@@ -70,13 +70,13 @@ const CodexToast = (): JSX.Element | null => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="achievement-toast-container">
|
<>
|
||||||
{pendingEntryIds.map((id) => {
|
{pendingEntryIds.map((id) => {
|
||||||
return (
|
return (
|
||||||
<CodexToastItem entryId={id} key={id} onDismiss={dismissCodexEntry} />
|
<CodexToastItem entryId={id} key={id} onDismiss={dismissCodexEntry} />
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -27,10 +27,12 @@ import { EditProfileModal } from "./editProfileModal.js";
|
|||||||
import { EquipmentPanel } from "./equipmentPanel.js";
|
import { EquipmentPanel } from "./equipmentPanel.js";
|
||||||
import { ExplorationPanel } from "./explorationPanel.js";
|
import { ExplorationPanel } from "./explorationPanel.js";
|
||||||
import { LoginBonusModal } from "./loginBonusModal.js";
|
import { LoginBonusModal } from "./loginBonusModal.js";
|
||||||
|
import { MilestoneToast } from "./milestoneToast.js";
|
||||||
import { OfflineModal } from "./offlineModal.js";
|
import { OfflineModal } from "./offlineModal.js";
|
||||||
import { OutdatedSchemaModal } from "./outdatedSchemaModal.js";
|
import { OutdatedSchemaModal } from "./outdatedSchemaModal.js";
|
||||||
import { PrestigePanel } from "./prestigePanel.js";
|
import { PrestigePanel } from "./prestigePanel.js";
|
||||||
import { QuestPanel } from "./questPanel.js";
|
import { QuestPanel } from "./questPanel.js";
|
||||||
|
import { QuestCompleteToast, QuestFailedToast } from "./questToast.js";
|
||||||
import { StatisticsPanel } from "./statisticsPanel.js";
|
import { StatisticsPanel } from "./statisticsPanel.js";
|
||||||
import { StoryPanel } from "./storyPanel.js";
|
import { StoryPanel } from "./storyPanel.js";
|
||||||
import { StoryToast } from "./storyToast.js";
|
import { StoryToast } from "./storyToast.js";
|
||||||
@@ -164,9 +166,14 @@ const GameLayout = (): JSX.Element => {
|
|||||||
{schemaOutdated && !dismissedOutdatedWarning
|
{schemaOutdated && !dismissedOutdatedWarning
|
||||||
? <OutdatedSchemaModal onDismiss={handleDismissOutdated} />
|
? <OutdatedSchemaModal onDismiss={handleDismissOutdated} />
|
||||||
: null}
|
: null}
|
||||||
<AchievementToast />
|
<div className="achievement-toast-container">
|
||||||
<CodexToast />
|
<AchievementToast />
|
||||||
<StoryToast />
|
<CodexToast />
|
||||||
|
<MilestoneToast />
|
||||||
|
<QuestCompleteToast />
|
||||||
|
<QuestFailedToast />
|
||||||
|
<StoryToast />
|
||||||
|
</div>
|
||||||
{loginBonus === null
|
{loginBonus === null
|
||||||
? null
|
? null
|
||||||
: <LoginBonusModal bonus={loginBonus} onClose={dismissLoginBonus} />
|
: <LoginBonusModal bonus={loginBonus} onClose={dismissLoginBonus} />
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
/**
|
||||||
|
* @file Milestone toast notification component for prestige, transcendence, and apotheosis.
|
||||||
|
* @copyright nhcarrigan
|
||||||
|
* @license Naomi's Public License
|
||||||
|
* @author Naomi Carrigan
|
||||||
|
*/
|
||||||
|
/* eslint-disable react/no-multi-comp -- Sub-components are tightly coupled to their containers */
|
||||||
|
import { type JSX, useEffect } from "react";
|
||||||
|
import { useGame } from "../../context/gameContext.js";
|
||||||
|
|
||||||
|
interface MilestoneToastItemProperties {
|
||||||
|
readonly icon: string;
|
||||||
|
readonly label: string;
|
||||||
|
readonly onDismiss: ()=> void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a single milestone toast notification.
|
||||||
|
* @param props - The toast item properties.
|
||||||
|
* @param props.icon - The emoji icon.
|
||||||
|
* @param props.label - The label text.
|
||||||
|
* @param props.onDismiss - Callback to dismiss the toast.
|
||||||
|
* @returns The JSX element.
|
||||||
|
*/
|
||||||
|
const MilestoneToastItem = ({
|
||||||
|
icon,
|
||||||
|
label,
|
||||||
|
onDismiss,
|
||||||
|
}: MilestoneToastItemProperties): JSX.Element => {
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
onDismiss();
|
||||||
|
}, 4000);
|
||||||
|
return (): void => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
};
|
||||||
|
}, [ onDismiss ]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="game-toast" onClick={onDismiss}>
|
||||||
|
<span className="toast-icon">{icon}</span>
|
||||||
|
<div className="toast-content">
|
||||||
|
<span className="toast-label">{label}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders all milestone toasts (prestige, transcendence, apotheosis).
|
||||||
|
* @returns The JSX element or null if no milestone toasts are pending.
|
||||||
|
*/
|
||||||
|
const MilestoneToast = (): JSX.Element | null => {
|
||||||
|
const {
|
||||||
|
showPrestigeToast,
|
||||||
|
showTranscendenceToast,
|
||||||
|
showApotheosisToast,
|
||||||
|
dismissPrestigeToast,
|
||||||
|
dismissTranscendenceToast,
|
||||||
|
dismissApotheosisToast,
|
||||||
|
} = useGame();
|
||||||
|
|
||||||
|
const hasAny
|
||||||
|
= showPrestigeToast || showTranscendenceToast || showApotheosisToast;
|
||||||
|
if (!hasAny) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{showPrestigeToast
|
||||||
|
? <MilestoneToastItem
|
||||||
|
icon={"⭐"}
|
||||||
|
label={"⭐ Prestige!"}
|
||||||
|
onDismiss={dismissPrestigeToast}
|
||||||
|
/>
|
||||||
|
: null}
|
||||||
|
{showTranscendenceToast
|
||||||
|
? <MilestoneToastItem
|
||||||
|
icon={"🌌"}
|
||||||
|
label={"🌌 Transcendence!"}
|
||||||
|
onDismiss={dismissTranscendenceToast}
|
||||||
|
/>
|
||||||
|
: null}
|
||||||
|
{showApotheosisToast
|
||||||
|
? <MilestoneToastItem
|
||||||
|
icon={"✨"}
|
||||||
|
label={"✨ Apotheosis!"}
|
||||||
|
onDismiss={dismissApotheosisToast}
|
||||||
|
/>
|
||||||
|
: null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { MilestoneToast };
|
||||||
@@ -89,6 +89,7 @@ const PrestigePanel = (): JSX.Element => {
|
|||||||
enableNotifications,
|
enableNotifications,
|
||||||
enableSounds,
|
enableSounds,
|
||||||
toggleAutoPrestige,
|
toggleAutoPrestige,
|
||||||
|
triggerPrestigeToast,
|
||||||
} = useGame();
|
} = useGame();
|
||||||
const [ isPending, setIsPending ] = useState(false);
|
const [ isPending, setIsPending ] = useState(false);
|
||||||
const [ result, setResult ] = useState<{
|
const [ result, setResult ] = useState<{
|
||||||
@@ -128,6 +129,7 @@ const PrestigePanel = (): JSX.Element => {
|
|||||||
milestoneRunestones: data.milestoneRunestones,
|
milestoneRunestones: data.milestoneRunestones,
|
||||||
runestones: data.runestones,
|
runestones: data.runestones,
|
||||||
});
|
});
|
||||||
|
triggerPrestigeToast();
|
||||||
if (enableSounds) {
|
if (enableSounds) {
|
||||||
playSound("prestige");
|
playSound("prestige");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -190,10 +190,11 @@ const QuestPanel = (): JSX.Element => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { adventurers, autoQuest, quests, zones } = state;
|
const { adventurers, autoQuest, quests, zones } = state;
|
||||||
// eslint-disable-next-line unicorn/no-array-reduce -- Need the total!
|
let partyCombatPower = 0;
|
||||||
const partyCombatPower = adventurers.reduce((total, adventurer) => {
|
for (const adventurer of adventurers) {
|
||||||
return total + adventurer.combatPower * adventurer.count;
|
const contribution = adventurer.combatPower * adventurer.count;
|
||||||
}, 0);
|
partyCombatPower = partyCombatPower + contribution;
|
||||||
|
}
|
||||||
const zoneQuests = quests.filter(({ zoneId }) => {
|
const zoneQuests = quests.filter(({ zoneId }) => {
|
||||||
return zoneId === activeZoneId;
|
return zoneId === activeZoneId;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
/**
|
||||||
|
* @file Quest toast notification component for completed and failed quests.
|
||||||
|
* @copyright nhcarrigan
|
||||||
|
* @license Naomi's Public License
|
||||||
|
* @author Naomi Carrigan
|
||||||
|
*/
|
||||||
|
/* eslint-disable react/no-multi-comp -- Sub-components are tightly coupled to their containers */
|
||||||
|
import { type JSX, useEffect } from "react";
|
||||||
|
import { useGame } from "../../context/gameContext.js";
|
||||||
|
import type { Quest } from "@elysium/types";
|
||||||
|
|
||||||
|
interface QuestToastItemProperties {
|
||||||
|
readonly quest: Quest;
|
||||||
|
readonly onDismiss: (id: string)=> void;
|
||||||
|
// eslint-disable-next-line react/require-default-props -- Default value set in destructuring
|
||||||
|
readonly isFailure?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a single quest toast notification.
|
||||||
|
* @param props - The toast item properties.
|
||||||
|
* @param props.quest - The quest to display.
|
||||||
|
* @param props.onDismiss - Callback to dismiss the toast.
|
||||||
|
* @param props.isFailure - Whether this is a failure toast.
|
||||||
|
* @returns The JSX element.
|
||||||
|
*/
|
||||||
|
const QuestToastItem = ({
|
||||||
|
quest,
|
||||||
|
onDismiss,
|
||||||
|
isFailure = false,
|
||||||
|
}: QuestToastItemProperties): JSX.Element => {
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
onDismiss(quest.id);
|
||||||
|
}, 4000);
|
||||||
|
return (): void => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
};
|
||||||
|
}, [ quest.id, onDismiss ]);
|
||||||
|
|
||||||
|
function handleClick(): void {
|
||||||
|
onDismiss(quest.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="game-toast" onClick={handleClick}>
|
||||||
|
<span className="toast-icon">{isFailure
|
||||||
|
? "💀"
|
||||||
|
: "📜"}</span>
|
||||||
|
<div className="toast-content">
|
||||||
|
<span className="toast-label">{isFailure
|
||||||
|
? "Quest Failed!"
|
||||||
|
: "✨ Quest Complete!"}</span>
|
||||||
|
<span className="toast-name">{quest.name}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the quest complete toast container.
|
||||||
|
* @returns The JSX element or null if there are no pending quest toasts.
|
||||||
|
*/
|
||||||
|
const QuestCompleteToast = (): JSX.Element | null => {
|
||||||
|
const { completedQuestToasts, dismissCompletedQuest } = useGame();
|
||||||
|
|
||||||
|
if (completedQuestToasts.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{completedQuestToasts.map((quest) => {
|
||||||
|
return (
|
||||||
|
<QuestToastItem
|
||||||
|
key={quest.id}
|
||||||
|
onDismiss={dismissCompletedQuest}
|
||||||
|
quest={quest}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the quest failed toast container.
|
||||||
|
* @returns The JSX element or null if there are no pending failure toasts.
|
||||||
|
*/
|
||||||
|
const QuestFailedToast = (): JSX.Element | null => {
|
||||||
|
const { failedQuestToasts, dismissFailedQuest } = useGame();
|
||||||
|
|
||||||
|
if (failedQuestToasts.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{failedQuestToasts.map((quest) => {
|
||||||
|
return (
|
||||||
|
<QuestToastItem
|
||||||
|
isFailure={true}
|
||||||
|
key={quest.id}
|
||||||
|
onDismiss={dismissFailedQuest}
|
||||||
|
quest={quest}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { QuestCompleteToast, QuestFailedToast };
|
||||||
@@ -45,13 +45,13 @@ const StoryToastItem = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button className="achievement-toast" onClick={handleClick} type="button">
|
<div className="game-toast" onClick={handleClick}>
|
||||||
<span className="achievement-toast-icon">{"📖"}</span>
|
<span className="toast-icon">{"📖"}</span>
|
||||||
<div className="achievement-toast-content">
|
<div className="toast-content">
|
||||||
<span className="achievement-toast-label">{"✨ New Chapter!"}</span>
|
<span className="toast-label">{"✨ New Chapter!"}</span>
|
||||||
<span className="achievement-toast-name">{chapter.title}</span>
|
<span className="toast-name">{chapter.title}</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -65,11 +65,11 @@ const StoryToast = (): JSX.Element | null => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="achievement-toast-container">
|
<>
|
||||||
{pendingChapterIds.map((id) => {
|
{pendingChapterIds.map((id) => {
|
||||||
return <StoryToastItem chapterId={id} key={id} />;
|
return <StoryToastItem chapterId={id} key={id} />;
|
||||||
})}
|
})}
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
type GameState,
|
type GameState,
|
||||||
type LoginBonusResult,
|
type LoginBonusResult,
|
||||||
type NumberFormat,
|
type NumberFormat,
|
||||||
|
type Quest,
|
||||||
type TranscendenceResponse,
|
type TranscendenceResponse,
|
||||||
isStoryChapterUnlocked,
|
isStoryChapterUnlocked,
|
||||||
} from "@elysium/types";
|
} from "@elysium/types";
|
||||||
@@ -334,6 +335,61 @@ interface GameContextValue {
|
|||||||
*/
|
*/
|
||||||
dismissAchievement: (id: string)=> void;
|
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.
|
* The player's chosen number display format.
|
||||||
*/
|
*/
|
||||||
@@ -514,6 +570,15 @@ export const GameProvider = ({
|
|||||||
const [ unlockedAchievements, setUnlockedAchievements ] = useState<
|
const [ unlockedAchievements, setUnlockedAchievements ] = useState<
|
||||||
Array<Achievement>
|
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 [ lastSavedAt, setLastSavedAt ] = useState<number | null>(null);
|
||||||
const [ isSyncing, setIsSyncing ] = useState(false);
|
const [ isSyncing, setIsSyncing ] = useState(false);
|
||||||
const [ syncError, setSyncError ] = useState<string | null>(null);
|
const [ syncError, setSyncError ] = useState<string | null>(null);
|
||||||
@@ -530,8 +595,8 @@ export const GameProvider = ({
|
|||||||
const isSyncingReference = useRef(false);
|
const isSyncingReference = useRef(false);
|
||||||
const rafReference = useRef<number | null>(null);
|
const rafReference = useRef<number | null>(null);
|
||||||
const unlockedAchievementsReference = useRef<Array<Achievement>>([]);
|
const unlockedAchievementsReference = useRef<Array<Achievement>>([]);
|
||||||
const newlyCompletedQuestsCountReference = useRef(0);
|
const newlyCompletedQuestsReference = useRef<Array<Quest>>([]);
|
||||||
const newlyFailedQuestsCountReference = useRef(0);
|
const newlyFailedQuestsReference = useRef<Array<Quest>>([]);
|
||||||
const signatureReference = useRef<string | null>(
|
const signatureReference = useRef<string | null>(
|
||||||
localStorage.getItem("elysium_save_signature"),
|
localStorage.getItem("elysium_save_signature"),
|
||||||
);
|
);
|
||||||
@@ -949,17 +1014,17 @@ export const GameProvider = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Detect newly completed quests
|
// Detect newly completed quests
|
||||||
newlyCompletedQuestsCountReference.current = next.quests.filter(
|
newlyCompletedQuestsReference.current = next.quests.filter(
|
||||||
(q, index) => {
|
(q, index) => {
|
||||||
return (
|
return (
|
||||||
previous.quests[index]?.status === "active"
|
previous.quests[index]?.status === "active"
|
||||||
&& q.status === "completed"
|
&& q.status === "completed"
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
).length;
|
);
|
||||||
|
|
||||||
// Detect newly failed quests
|
// Detect newly failed quests
|
||||||
newlyFailedQuestsCountReference.current = next.quests.filter(
|
newlyFailedQuestsReference.current = next.quests.filter(
|
||||||
(q, index) => {
|
(q, index) => {
|
||||||
const previousFailedAt = previous.quests[index]?.lastFailedAt;
|
const previousFailedAt = previous.quests[index]?.lastFailedAt;
|
||||||
return (
|
return (
|
||||||
@@ -967,7 +1032,7 @@ export const GameProvider = ({
|
|||||||
&& q.lastFailedAt !== previousFailedAt
|
&& q.lastFailedAt !== previousFailedAt
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
).length;
|
);
|
||||||
|
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
@@ -987,24 +1052,30 @@ export const GameProvider = ({
|
|||||||
unlockedAchievementsReference.current = [];
|
unlockedAchievementsReference.current = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newlyCompletedQuestsCountReference.current > 0) {
|
if (newlyCompletedQuestsReference.current.length > 0) {
|
||||||
|
setCompletedQuestToasts((previous) => {
|
||||||
|
return [ ...previous, ...newlyCompletedQuestsReference.current ];
|
||||||
|
});
|
||||||
if (enableSoundsReference.current) {
|
if (enableSoundsReference.current) {
|
||||||
playSound("questCompleted");
|
playSound("questCompleted");
|
||||||
}
|
}
|
||||||
if (enableNotificationsReference.current) {
|
if (enableNotificationsReference.current) {
|
||||||
sendNotification("📜 Quest Complete!", "A quest has been completed.");
|
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) {
|
if (enableSoundsReference.current) {
|
||||||
playSound("questFailed");
|
playSound("questFailed");
|
||||||
}
|
}
|
||||||
if (enableNotificationsReference.current) {
|
if (enableNotificationsReference.current) {
|
||||||
sendNotification("💀 Quest Failed!", "A quest has failed.");
|
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)
|
// 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;
|
isAutoPrestigingReference.current = true;
|
||||||
void prestigeApi({}).
|
void prestigeApi({}).
|
||||||
then(async() => {
|
then(async() => {
|
||||||
|
setShowPrestigeToast(true);
|
||||||
if (enableSoundsReference.current) {
|
if (enableSoundsReference.current) {
|
||||||
playSound("prestige");
|
playSound("prestige");
|
||||||
}
|
}
|
||||||
@@ -1443,6 +1515,7 @@ export const GameProvider = ({
|
|||||||
|
|
||||||
const transcend = useCallback(async() => {
|
const transcend = useCallback(async() => {
|
||||||
const result = await transcendApi({});
|
const result = await transcendApi({});
|
||||||
|
setShowTranscendenceToast(true);
|
||||||
if (enableSoundsReference.current) {
|
if (enableSoundsReference.current) {
|
||||||
playSound("transcendence");
|
playSound("transcendence");
|
||||||
}
|
}
|
||||||
@@ -1455,6 +1528,7 @@ export const GameProvider = ({
|
|||||||
|
|
||||||
const apotheosis = useCallback(async() => {
|
const apotheosis = useCallback(async() => {
|
||||||
const result = await achieveApotheosisApi({});
|
const result = await achieveApotheosisApi({});
|
||||||
|
setShowApotheosisToast(true);
|
||||||
if (enableSoundsReference.current) {
|
if (enableSoundsReference.current) {
|
||||||
playSound("apotheosis");
|
playSound("apotheosis");
|
||||||
}
|
}
|
||||||
@@ -1733,6 +1807,38 @@ export const GameProvider = ({
|
|||||||
setBattleResult(null);
|
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) => {
|
const dismissAchievement = useCallback((id: string) => {
|
||||||
setUnlockedAchievements((previous) => {
|
setUnlockedAchievements((previous) => {
|
||||||
return previous.filter((a) => {
|
return previous.filter((a) => {
|
||||||
@@ -1829,18 +1935,25 @@ export const GameProvider = ({
|
|||||||
challengeBoss,
|
challengeBoss,
|
||||||
collectExploration,
|
collectExploration,
|
||||||
completeChapter,
|
completeChapter,
|
||||||
|
completedQuestToasts,
|
||||||
craftRecipe,
|
craftRecipe,
|
||||||
currentSchemaVersion,
|
currentSchemaVersion,
|
||||||
dismissAchievement,
|
dismissAchievement,
|
||||||
|
dismissApotheosisToast,
|
||||||
dismissBattle,
|
dismissBattle,
|
||||||
dismissCodexEntry,
|
dismissCodexEntry,
|
||||||
|
dismissCompletedQuest,
|
||||||
|
dismissFailedQuest,
|
||||||
dismissLoginBonus,
|
dismissLoginBonus,
|
||||||
dismissOfflineGold,
|
dismissOfflineGold,
|
||||||
|
dismissPrestigeToast,
|
||||||
dismissStoryChapter,
|
dismissStoryChapter,
|
||||||
|
dismissTranscendenceToast,
|
||||||
enableNotifications,
|
enableNotifications,
|
||||||
enableSounds,
|
enableSounds,
|
||||||
equipItem,
|
equipItem,
|
||||||
error,
|
error,
|
||||||
|
failedQuestToasts,
|
||||||
forceSync,
|
forceSync,
|
||||||
formatNumber,
|
formatNumber,
|
||||||
handleClick,
|
handleClick,
|
||||||
@@ -1860,6 +1973,9 @@ export const GameProvider = ({
|
|||||||
setEnableNotifications,
|
setEnableNotifications,
|
||||||
setEnableSounds,
|
setEnableSounds,
|
||||||
setNumberFormat,
|
setNumberFormat,
|
||||||
|
showApotheosisToast,
|
||||||
|
showPrestigeToast,
|
||||||
|
showTranscendenceToast,
|
||||||
startExploration,
|
startExploration,
|
||||||
startQuest,
|
startQuest,
|
||||||
state,
|
state,
|
||||||
@@ -1868,6 +1984,7 @@ export const GameProvider = ({
|
|||||||
toggleAutoPrestige,
|
toggleAutoPrestige,
|
||||||
toggleAutoQuest,
|
toggleAutoQuest,
|
||||||
transcend,
|
transcend,
|
||||||
|
triggerPrestigeToast,
|
||||||
unlockedAchievements,
|
unlockedAchievements,
|
||||||
unlockedCodexEntryIds,
|
unlockedCodexEntryIds,
|
||||||
unlockedStoryChapterIds,
|
unlockedStoryChapterIds,
|
||||||
@@ -1875,6 +1992,8 @@ export const GameProvider = ({
|
|||||||
}, [
|
}, [
|
||||||
apotheosis,
|
apotheosis,
|
||||||
battleResult,
|
battleResult,
|
||||||
|
completedQuestToasts,
|
||||||
|
failedQuestToasts,
|
||||||
formatNumber,
|
formatNumber,
|
||||||
buyAdventurer,
|
buyAdventurer,
|
||||||
buyEchoUpgrade,
|
buyEchoUpgrade,
|
||||||
@@ -1887,11 +2006,16 @@ export const GameProvider = ({
|
|||||||
craftRecipe,
|
craftRecipe,
|
||||||
currentSchemaVersion,
|
currentSchemaVersion,
|
||||||
dismissAchievement,
|
dismissAchievement,
|
||||||
|
dismissApotheosisToast,
|
||||||
dismissBattle,
|
dismissBattle,
|
||||||
dismissCodexEntry,
|
dismissCodexEntry,
|
||||||
|
dismissCompletedQuest,
|
||||||
|
dismissFailedQuest,
|
||||||
dismissLoginBonus,
|
dismissLoginBonus,
|
||||||
dismissOfflineGold,
|
dismissOfflineGold,
|
||||||
|
dismissPrestigeToast,
|
||||||
dismissStoryChapter,
|
dismissStoryChapter,
|
||||||
|
dismissTranscendenceToast,
|
||||||
enableNotifications,
|
enableNotifications,
|
||||||
enableSounds,
|
enableSounds,
|
||||||
equipItem,
|
equipItem,
|
||||||
@@ -1914,6 +2038,9 @@ export const GameProvider = ({
|
|||||||
setEnableNotifications,
|
setEnableNotifications,
|
||||||
setEnableSounds,
|
setEnableSounds,
|
||||||
setNumberFormat,
|
setNumberFormat,
|
||||||
|
showApotheosisToast,
|
||||||
|
showPrestigeToast,
|
||||||
|
showTranscendenceToast,
|
||||||
startExploration,
|
startExploration,
|
||||||
startQuest,
|
startQuest,
|
||||||
state,
|
state,
|
||||||
@@ -1922,6 +2049,7 @@ export const GameProvider = ({
|
|||||||
toggleAutoPrestige,
|
toggleAutoPrestige,
|
||||||
toggleAutoQuest,
|
toggleAutoQuest,
|
||||||
transcend,
|
transcend,
|
||||||
|
triggerPrestigeToast,
|
||||||
unlockedAchievements,
|
unlockedAchievements,
|
||||||
unlockedCodexEntryIds,
|
unlockedCodexEntryIds,
|
||||||
unlockedStoryChapterIds,
|
unlockedStoryChapterIds,
|
||||||
|
|||||||
+2
-16
@@ -1432,20 +1432,6 @@ body {
|
|||||||
z-index: 200;
|
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 {
|
.toast-icon {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
@@ -2481,8 +2467,8 @@ body {
|
|||||||
padding: 0.6rem 0.75rem;
|
padding: 0.6rem 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Codex toast — uses a different accent from achievement toast */
|
/* Unified game toast — essence-coloured border used by all in-game notifications */
|
||||||
.codex-toast {
|
.game-toast {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
animation: slide-in-right 0.35s ease-out;
|
animation: slide-in-right 0.35s ease-out;
|
||||||
background: var(--colour-surface);
|
background: var(--colour-surface);
|
||||||
|
|||||||
Reference in New Issue
Block a user