generated from nhcarrigan/template
feat: achievement timestamps, companion progress bars, max runestone indicator, and auto-prestige max-runes mode
This commit is contained in:
@@ -156,7 +156,18 @@ const AchievementCard = ({
|
||||
</div>
|
||||
<div className="achievement-status">
|
||||
{isUnlocked
|
||||
? <span className="achievement-unlocked-badge">{"✓ Unlocked"}</span>
|
||||
? <>
|
||||
<span className="achievement-unlocked-badge">{"✓ Unlocked"}</span>
|
||||
{achievement.unlockedAt !== null
|
||||
&& <span className="achievement-unlocked-at">
|
||||
{new Date(achievement.unlockedAt).toLocaleDateString("en-GB", {
|
||||
day: "numeric",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
})}
|
||||
</span>
|
||||
}
|
||||
</>
|
||||
: <span className="achievement-locked-badge">{"🔒"}</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -30,11 +30,12 @@ const unlockLabels: Record<string, string> = {
|
||||
};
|
||||
|
||||
interface CompanionCardProperties {
|
||||
readonly companion: Companion;
|
||||
readonly isUnlocked: boolean;
|
||||
readonly isActive: boolean;
|
||||
readonly onSelect: ()=> void;
|
||||
readonly formatNumber: (n: number)=> string;
|
||||
readonly companion: Companion;
|
||||
readonly isUnlocked: boolean;
|
||||
readonly isActive: boolean;
|
||||
readonly onSelect: ()=> void;
|
||||
readonly formatNumber: (n: number)=> string;
|
||||
readonly currentProgress: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,6 +46,7 @@ interface CompanionCardProperties {
|
||||
* @param props.isActive - Whether this companion is currently active.
|
||||
* @param props.onSelect - Callback when the companion is selected/deselected.
|
||||
* @param props.formatNumber - The number formatting utility function.
|
||||
* @param props.currentProgress - The player's current progress toward the unlock threshold.
|
||||
* @returns The JSX element.
|
||||
*/
|
||||
const CompanionCard = ({
|
||||
@@ -53,6 +55,7 @@ const CompanionCard = ({
|
||||
isActive,
|
||||
onSelect,
|
||||
formatNumber,
|
||||
currentProgress,
|
||||
}: CompanionCardProperties): JSX.Element => {
|
||||
const bonusSign = companion.bonus.type === "questTime"
|
||||
? "-"
|
||||
@@ -111,11 +114,28 @@ const CompanionCard = ({
|
||||
: "Activate"}
|
||||
</button>
|
||||
: <div className="companion-unlock-requirement">
|
||||
{"🔒 Unlock: "}
|
||||
{companion.unlock.type === "lifetimeGold"
|
||||
? formatNumber(companion.unlock.threshold)
|
||||
: String(companion.unlock.threshold)}{" "}
|
||||
{unlockLabels[companion.unlock.type] ?? companion.unlock.type}
|
||||
<p>
|
||||
{"🔒 Unlock: "}
|
||||
{companion.unlock.type === "lifetimeGold"
|
||||
? formatNumber(companion.unlock.threshold)
|
||||
: String(companion.unlock.threshold)}{" "}
|
||||
{unlockLabels[companion.unlock.type] ?? companion.unlock.type}
|
||||
</p>
|
||||
<div className="companion-progress">
|
||||
<progress
|
||||
max={companion.unlock.threshold}
|
||||
value={Math.min(currentProgress, companion.unlock.threshold)}
|
||||
/>
|
||||
<span className="companion-progress-label">
|
||||
{companion.unlock.type === "lifetimeGold"
|
||||
? formatNumber(currentProgress)
|
||||
: String(currentProgress)}
|
||||
{" / "}
|
||||
{companion.unlock.type === "lifetimeGold"
|
||||
? formatNumber(companion.unlock.threshold)
|
||||
: String(companion.unlock.threshold)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -140,6 +160,15 @@ const CompanionPanel = (): JSX.Element => {
|
||||
const unlockedIds = state.companions?.unlockedCompanionIds ?? [];
|
||||
const activeId = state.companions?.activeCompanionId ?? null;
|
||||
|
||||
const progressByUnlockType: Record<string, number> = {
|
||||
apotheosis: state.apotheosis?.count ?? 0,
|
||||
lifetimeBosses: state.player.lifetimeBossesDefeated,
|
||||
lifetimeGold: state.player.lifetimeGoldEarned,
|
||||
lifetimeQuests: state.player.lifetimeQuestsCompleted,
|
||||
prestige: state.prestige.count,
|
||||
transcendence: state.transcendence?.count ?? 0,
|
||||
};
|
||||
|
||||
function handleSelect(companionId: string): void {
|
||||
setActiveCompanion(activeId === companionId
|
||||
? null
|
||||
@@ -177,6 +206,9 @@ const CompanionPanel = (): JSX.Element => {
|
||||
return (
|
||||
<CompanionCard
|
||||
companion={companion}
|
||||
currentProgress={
|
||||
progressByUnlockType[companion.unlock.type] ?? 0
|
||||
}
|
||||
formatNumber={formatNumber}
|
||||
isActive={activeId === companion.id}
|
||||
isUnlocked={unlockedIds.includes(companion.id)}
|
||||
|
||||
@@ -68,6 +68,7 @@ const PrestigePanel = (): JSX.Element => {
|
||||
enableSounds,
|
||||
toggleAutoAdventurer,
|
||||
toggleAutoPrestige,
|
||||
toggleAutoPrestigeMaxRunestones,
|
||||
triggerPrestigeToast,
|
||||
} = useGame();
|
||||
const [ isPending, setIsPending ] = useState(false);
|
||||
@@ -93,6 +94,11 @@ const PrestigePanel = (): JSX.Element => {
|
||||
const isEligible = player.totalGoldEarned >= threshold;
|
||||
const runestonePreview = computeProjectedRunestones(state);
|
||||
const nextMultiplier = calculateProductionMultiplier(prestigeData.count + 1);
|
||||
const baseRunestones = Math.min(
|
||||
Math.floor(Math.cbrt(player.totalGoldEarned / threshold)) * 15,
|
||||
200,
|
||||
);
|
||||
const isAtMaxRunestones = baseRunestones >= 200;
|
||||
|
||||
async function handlePrestige(): Promise<void> {
|
||||
setIsPending(true);
|
||||
@@ -155,6 +161,10 @@ const PrestigePanel = (): JSX.Element => {
|
||||
toggleAutoPrestige();
|
||||
}
|
||||
|
||||
function handleAutoPrestigeMaxRunestonesToggle(): void {
|
||||
toggleAutoPrestigeMaxRunestones();
|
||||
}
|
||||
|
||||
function handlePrestigeTabClick(): void {
|
||||
setActiveTab("prestige");
|
||||
}
|
||||
@@ -241,6 +251,16 @@ const PrestigePanel = (): JSX.Element => {
|
||||
{"+"}
|
||||
{formatInteger(runestonePreview)}
|
||||
</strong>
|
||||
{isAtMaxRunestones
|
||||
? <span className="runestone-max-badge">{" ⚡ MAX"}</span>
|
||||
: null
|
||||
}
|
||||
</p>
|
||||
: null}
|
||||
{isEligible && !isAtMaxRunestones
|
||||
? <p className="runestone-progress-hint">
|
||||
{"Earn more gold to increase your runestone yield "
|
||||
+ "(capped at ×14³ the prestige threshold)."}
|
||||
</p>
|
||||
: null}
|
||||
{isEligible
|
||||
@@ -332,6 +352,8 @@ const PrestigePanel = (): JSX.Element => {
|
||||
= upgrade.id === "auto_prestige" && purchased;
|
||||
const autoPrestigeEnabled
|
||||
= prestigeData.autoPrestigeEnabled ?? false;
|
||||
const autoPrestigeMaxRunestonesOnly
|
||||
= prestigeData.autoPrestigeMaxRunestonesOnly ?? false;
|
||||
|
||||
function handleBuyClick(): void {
|
||||
void handleBuyUpgrade(upgrade.id);
|
||||
@@ -378,19 +400,37 @@ const PrestigePanel = (): JSX.Element => {
|
||||
</button>
|
||||
: null}
|
||||
{isAutoPrestigeToggle
|
||||
? <button
|
||||
className={`auto-prestige-toggle ${
|
||||
autoPrestigeEnabled
|
||||
? "enabled"
|
||||
: "disabled"
|
||||
}`}
|
||||
onClick={handleAutoPrestigeToggle}
|
||||
type="button"
|
||||
>
|
||||
? <>
|
||||
<button
|
||||
className={`auto-prestige-toggle ${
|
||||
autoPrestigeEnabled
|
||||
? "enabled"
|
||||
: "disabled"
|
||||
}`}
|
||||
onClick={handleAutoPrestigeToggle}
|
||||
type="button"
|
||||
>
|
||||
{autoPrestigeEnabled
|
||||
? "⚡ Auto ON"
|
||||
: "⏸ Auto OFF"}
|
||||
</button>
|
||||
{autoPrestigeEnabled
|
||||
? "⚡ Auto ON"
|
||||
: "⏸ Auto OFF"}
|
||||
</button>
|
||||
? <button
|
||||
className={`auto-prestige-toggle ${
|
||||
autoPrestigeMaxRunestonesOnly
|
||||
? "enabled"
|
||||
: "disabled"
|
||||
}`}
|
||||
onClick={handleAutoPrestigeMaxRunestonesToggle}
|
||||
title="Only fire at max runestone yield"
|
||||
type="button"
|
||||
>
|
||||
{autoPrestigeMaxRunestonesOnly
|
||||
? "⚡ Max Runes Only"
|
||||
: "⏸ Max Runes OFF"}
|
||||
</button>
|
||||
: null}
|
||||
</>
|
||||
: null}
|
||||
{purchased
|
||||
? null
|
||||
|
||||
@@ -479,6 +479,11 @@ interface GameContextValue {
|
||||
*/
|
||||
toggleAutoPrestige: ()=> void;
|
||||
|
||||
/**
|
||||
* Toggle whether auto-prestige waits for maximum runestone yield before firing.
|
||||
*/
|
||||
toggleAutoPrestigeMaxRunestones: ()=> void;
|
||||
|
||||
/**
|
||||
* Toggle the auto-quest setting on/off.
|
||||
*/
|
||||
@@ -1391,15 +1396,27 @@ export const GameProvider = ({
|
||||
|
||||
// Auto-prestige: fire when unlocked, enabled, and threshold is met
|
||||
const autoState = stateReference.current;
|
||||
const autoPrestigeThreshold = autoPrestigeThresholdBase
|
||||
* Math.pow((autoState?.prestige.count ?? 0) + 1, 2.5)
|
||||
* (autoState?.transcendence?.echoPrestigeThresholdMultiplier ?? 1);
|
||||
const autoBaseRunestones = Math.min(
|
||||
Math.floor(
|
||||
Math.cbrt(
|
||||
(autoState?.player.totalGoldEarned ?? 0) / autoPrestigeThreshold,
|
||||
),
|
||||
) * 15,
|
||||
200,
|
||||
);
|
||||
const autoMaxRunestonesMet
|
||||
= autoState?.prestige.autoPrestigeMaxRunestonesOnly !== true
|
||||
|| autoBaseRunestones >= 200;
|
||||
if (
|
||||
!isAutoPrestigingReference.current
|
||||
&& autoState?.prestige.purchasedUpgradeIds.includes("auto_prestige")
|
||||
=== true
|
||||
&& autoState.prestige.autoPrestigeEnabled === true
|
||||
&& autoState.player.totalGoldEarned
|
||||
>= autoPrestigeThresholdBase
|
||||
* Math.pow(autoState.prestige.count + 1, 2.5)
|
||||
* (autoState.transcendence?.echoPrestigeThresholdMultiplier ?? 1)
|
||||
&& autoState.player.totalGoldEarned >= autoPrestigeThreshold
|
||||
&& autoMaxRunestonesMet
|
||||
) {
|
||||
isAutoPrestigingReference.current = true;
|
||||
void prestigeApi({}).
|
||||
@@ -2065,6 +2082,22 @@ export const GameProvider = ({
|
||||
});
|
||||
}, []);
|
||||
|
||||
const toggleAutoPrestigeMaxRunestones = useCallback(() => {
|
||||
setState((previous) => {
|
||||
if (previous === null) {
|
||||
return previous;
|
||||
}
|
||||
return {
|
||||
...previous,
|
||||
prestige: {
|
||||
...previous.prestige,
|
||||
autoPrestigeMaxRunestonesOnly:
|
||||
previous.prestige.autoPrestigeMaxRunestonesOnly !== true,
|
||||
},
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
const toggleAutoQuest = useCallback(() => {
|
||||
setState((previous) => {
|
||||
if (previous === null) {
|
||||
@@ -2496,6 +2529,7 @@ export const GameProvider = ({
|
||||
toggleAutoAdventurer,
|
||||
toggleAutoBoss,
|
||||
toggleAutoPrestige,
|
||||
toggleAutoPrestigeMaxRunestones,
|
||||
toggleAutoQuest,
|
||||
transcend,
|
||||
triggerPrestigeToast,
|
||||
@@ -2571,6 +2605,7 @@ export const GameProvider = ({
|
||||
toggleAutoAdventurer,
|
||||
toggleAutoBoss,
|
||||
toggleAutoPrestige,
|
||||
toggleAutoPrestigeMaxRunestones,
|
||||
toggleAutoQuest,
|
||||
transcend,
|
||||
triggerPrestigeToast,
|
||||
|
||||
Reference in New Issue
Block a user