Files
elysium/apps/web/src/components/game/DailyChallengePanel.tsx
T
hikari aaeece1a18 feat: add daily challenges system
Three PST-midnight-resetting challenges generated deterministically per
day from click, boss, quest, and prestige types. Progress tracked
server-side for bosses and prestige, client-side for clicks and quests.
Crystal rewards awarded on completion and preserved through prestige resets.
2026-03-06 22:22:18 -08:00

92 lines
3.4 KiB
TypeScript

import { useGame } from "../../context/GameContext.js";
const formatTimeUntilReset = (): string => {
const now = new Date();
// Mirror the server's PST/PDT-based rollover: challenges reset at PST midnight
const nowAsPST = new Date(now.toLocaleString("en-US", { timeZone: "America/Los_Angeles" }));
const tomorrowMidnightPST = new Date(nowAsPST);
tomorrowMidnightPST.setDate(tomorrowMidnightPST.getDate() + 1);
tomorrowMidnightPST.setHours(0, 0, 0, 0);
const pstOffset = nowAsPST.getTime() - now.getTime();
const resetAt = new Date(tomorrowMidnightPST.getTime() - pstOffset);
const msRemaining = resetAt.getTime() - now.getTime();
const hoursRemaining = Math.floor(msRemaining / (1000 * 60 * 60));
const minutesRemaining = Math.floor((msRemaining % (1000 * 60 * 60)) / (1000 * 60));
return `${String(hoursRemaining)}h ${String(minutesRemaining)}m`;
};
export const DailyChallengePanel = (): React.JSX.Element => {
const { state, formatNumber } = useGame();
if (!state) return <section className="panel"><p>Loading...</p></section>;
const { dailyChallenges } = state;
if (!dailyChallenges) {
return (
<section className="panel daily-challenge-panel">
<h2>📅 Daily Challenges</h2>
<p className="daily-challenge-subtitle">Load the game to generate today's challenges!</p>
</section>
);
}
const completedCount = dailyChallenges.challenges.filter((c) => c.completed).length;
return (
<section className="panel daily-challenge-panel">
<h2>📅 Daily Challenges</h2>
<div className="daily-challenge-header">
<p className="daily-challenge-subtitle">
Complete challenges for bonus 💎 crystals! Resets in{" "}
<strong>{formatTimeUntilReset()}</strong> (PST midnight).
</p>
<p className="daily-challenge-progress">
{completedCount} / {dailyChallenges.challenges.length} completed
</p>
</div>
<div className="daily-challenge-list">
{dailyChallenges.challenges.map((challenge) => {
const progressPercent = Math.min(
100,
Math.floor((challenge.progress / challenge.target) * 100),
);
return (
<div
key={challenge.id}
className={`daily-challenge-card ${challenge.completed ? "completed" : ""}`}
>
<div className="daily-challenge-info">
<h3 className="daily-challenge-label">{challenge.label}</h3>
<p className="daily-challenge-reward">
Reward: <strong>💎 {formatNumber(challenge.rewardCrystals)} crystals</strong>
</p>
</div>
<div className="daily-challenge-right">
{challenge.completed ? (
<span className="daily-challenge-done">✅ Complete!</span>
) : (
<>
<p className="daily-challenge-count">
{formatNumber(challenge.progress)} / {formatNumber(challenge.target)}
</p>
<div className="daily-challenge-bar-track">
<div
className="daily-challenge-bar-fill"
style={{ width: `${String(progressPercent)}%` }}
/>
</div>
</>
)}
</div>
</div>
);
})}
</div>
</section>
);
};