generated from nhcarrigan/template
feat: initial prototype — core game systems (#30)
## Summary This PR represents the full v1 prototype, implementing the core game systems for Elysium. - Full idle/clicker RPG loop: resource collection, crafting, boss fights, exploration, and quests - Adventurer hiring with batch size selector and progressive tier cost scaling - Prestige, transcendence, and apotheosis systems with auto-prestige support - Character sheet, titles, leaderboards, companion system, and daily login bonuses - Auto-quest and auto-boss toggles - Discord webhook notifications on prestige/transcendence/apotheosis - Discord role awarded on apotheosis - Responsive design and overarching story/lore system - In-game sound effects and browser notifications for key events - Support link button in the resource bar - Full test coverage (100% on `apps/api` and `packages/types`) - CI pipeline: lint → build → test ## Closes Closes #1 Closes #2 Closes #3 Closes #4 Closes #5 Closes #6 Closes #7 Closes #8 Closes #9 Closes #10 Closes #11 Closes #12 Closes #13 Closes #14 Closes #16 Closes #19 Closes #20 Closes #21 Closes #22 Closes #23 Closes #24 Closes #25 Closes #26 Closes #27 Closes #29 ✨ This issue was created with help from Hikari~ 🌸 Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com> Reviewed-on: #30 Co-authored-by: Hikari <hikari@nhcarrigan.com> Co-committed-by: Hikari <hikari@nhcarrigan.com>
This commit was merged in pull request #30.
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* @file Daily challenge panel component showing today's challenges.
|
||||
* @copyright nhcarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
/* eslint-disable max-lines-per-function -- Complex component with many render paths */
|
||||
import { useGame } from "../../context/gameContext.js";
|
||||
import type { JSX } from "react";
|
||||
|
||||
/**
|
||||
* Formats the time remaining until the daily reset.
|
||||
* @returns The formatted time string.
|
||||
*/
|
||||
const formatTimeUntilReset = (): string => {
|
||||
const now = new Date();
|
||||
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 msPerHour = 1000 * 60 * 60;
|
||||
const msPerMinute = 1000 * 60;
|
||||
const hoursRemaining = Math.floor(msRemaining / msPerHour);
|
||||
const msAfterHours = msRemaining % msPerHour;
|
||||
const minutesRemaining = Math.floor(msAfterHours / msPerMinute);
|
||||
return `${String(hoursRemaining)}h ${String(minutesRemaining)}m`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the daily challenge panel with progress tracking.
|
||||
* @returns The JSX element.
|
||||
*/
|
||||
const DailyChallengePanel = (): JSX.Element => {
|
||||
const { state, formatNumber } = useGame();
|
||||
|
||||
if (state === null) {
|
||||
return (
|
||||
<section className="panel">
|
||||
<p>{"Loading..."}</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
const { dailyChallenges } = state;
|
||||
|
||||
if (dailyChallenges === undefined) {
|
||||
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((challenge) => {
|
||||
return challenge.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 progressScaled = challenge.progress * 100;
|
||||
const progressPercent = Math.min(
|
||||
100,
|
||||
Math.floor(progressScaled / challenge.target),
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`daily-challenge-card ${
|
||||
challenge.completed
|
||||
? "completed"
|
||||
: ""
|
||||
}`}
|
||||
key={challenge.id}
|
||||
>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
export { DailyChallengePanel };
|
||||
Reference in New Issue
Block a user