diff --git a/apps/api/src/services/prestige.ts b/apps/api/src/services/prestige.ts
index 372c4ee..4fd106e 100644
--- a/apps/api/src/services/prestige.ts
+++ b/apps/api/src/services/prestige.ts
@@ -189,6 +189,7 @@ const buildPostPrestigeState = (
} => {
const {
autoPrestigeEnabled,
+ autoPrestigeMaxRunestonesOnly,
count: currentPrestigeCount,
purchasedUpgradeIds,
runestones: currentRunestones,
@@ -215,6 +216,9 @@ const buildPostPrestigeState = (
...autoPrestigeEnabled === undefined
? {}
: { autoPrestigeEnabled },
+ ...autoPrestigeMaxRunestonesOnly === undefined
+ ? {}
+ : { autoPrestigeMaxRunestonesOnly },
};
const freshState = initialGameState(currentState.player, characterName);
diff --git a/apps/api/test/services/prestige.spec.ts b/apps/api/test/services/prestige.spec.ts
index 8e7479e..90fbddf 100644
--- a/apps/api/test/services/prestige.spec.ts
+++ b/apps/api/test/services/prestige.spec.ts
@@ -255,6 +255,20 @@ describe("buildPostPrestigeState", () => {
expect(prestigeData.autoPrestigeEnabled).toBeUndefined();
});
+ it("preserves autoPrestigeMaxRunestonesOnly when set", () => {
+ const state = makeMinimalState({
+ prestige: { autoPrestigeMaxRunestonesOnly: true, count: 0, productionMultiplier: 1, purchasedUpgradeIds: [], runestones: 0 },
+ });
+ const { prestigeData } = buildPostPrestigeState(state, "Tester");
+ expect(prestigeData.autoPrestigeMaxRunestonesOnly).toBe(true);
+ });
+
+ it("omits autoPrestigeMaxRunestonesOnly when not set", () => {
+ const state = makeMinimalState();
+ const { prestigeData } = buildPostPrestigeState(state, "Tester");
+ expect(prestigeData.autoPrestigeMaxRunestonesOnly).toBeUndefined();
+ });
+
it("preserves apotheosis data across prestige", () => {
const apotheosis = { count: 2 };
const state = makeMinimalState({ apotheosis });
diff --git a/apps/web/src/components/game/achievementPanel.tsx b/apps/web/src/components/game/achievementPanel.tsx
index 8cb5d53..e718c91 100644
--- a/apps/web/src/components/game/achievementPanel.tsx
+++ b/apps/web/src/components/game/achievementPanel.tsx
@@ -156,7 +156,18 @@ const AchievementCard = ({
{isUnlocked
- ? {"✓ Unlocked"}
+ ? <>
+ {"✓ Unlocked"}
+ {achievement.unlockedAt !== null
+ &&
+ {new Date(achievement.unlockedAt).toLocaleDateString("en-GB", {
+ day: "numeric",
+ month: "short",
+ year: "numeric",
+ })}
+
+ }
+ >
: {"🔒"}
}
diff --git a/apps/web/src/components/game/companionPanel.tsx b/apps/web/src/components/game/companionPanel.tsx
index d664f80..a770a60 100644
--- a/apps/web/src/components/game/companionPanel.tsx
+++ b/apps/web/src/components/game/companionPanel.tsx
@@ -30,11 +30,12 @@ const unlockLabels: Record = {
};
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"}
:
- {"🔒 Unlock: "}
- {companion.unlock.type === "lifetimeGold"
- ? formatNumber(companion.unlock.threshold)
- : String(companion.unlock.threshold)}{" "}
- {unlockLabels[companion.unlock.type] ?? companion.unlock.type}
+
+ {"🔒 Unlock: "}
+ {companion.unlock.type === "lifetimeGold"
+ ? formatNumber(companion.unlock.threshold)
+ : String(companion.unlock.threshold)}{" "}
+ {unlockLabels[companion.unlock.type] ?? companion.unlock.type}
+
+
+
+
+ {companion.unlock.type === "lifetimeGold"
+ ? formatNumber(currentProgress)
+ : String(currentProgress)}
+ {" / "}
+ {companion.unlock.type === "lifetimeGold"
+ ? formatNumber(companion.unlock.threshold)
+ : String(companion.unlock.threshold)}
+
+
}
@@ -140,6 +160,15 @@ const CompanionPanel = (): JSX.Element => {
const unlockedIds = state.companions?.unlockedCompanionIds ?? [];
const activeId = state.companions?.activeCompanionId ?? null;
+ const progressByUnlockType: Record = {
+ 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 (
{
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 {
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)}
+ {isAtMaxRunestones
+ ? {" ⚡ MAX"}
+ : null
+ }
+
+ : null}
+ {isEligible && !isAtMaxRunestones
+ ?
+ {"Earn more gold to increase your runestone yield "
+ + "(capped at ×14³ the prestige threshold)."}
: 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 => {
: null}
{isAutoPrestigeToggle
- ?
+ ?
+ {autoPrestigeMaxRunestonesOnly
+ ? "⚡ Max Runes Only"
+ : "⏸ Max Runes OFF"}
+
+ : null}
+ >
: null}
{purchased
? null
diff --git a/apps/web/src/context/gameContext.tsx b/apps/web/src/context/gameContext.tsx
index cf16a9d..e54bd4e 100644
--- a/apps/web/src/context/gameContext.tsx
+++ b/apps/web/src/context/gameContext.tsx
@@ -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,
diff --git a/packages/types/src/interfaces/prestige.ts b/packages/types/src/interfaces/prestige.ts
index cab808c..1050a9e 100644
--- a/packages/types/src/interfaces/prestige.ts
+++ b/packages/types/src/interfaces/prestige.ts
@@ -56,6 +56,12 @@ interface PrestigeData {
* Whether the auto-prestige feature is currently enabled (requires auto_prestige upgrade).
*/
autoPrestigeEnabled?: boolean;
+
+ /**
+ * When true, auto-prestige only fires when the runestone yield is at its maximum base cap.
+ * Requires auto_prestige upgrade and autoPrestigeEnabled to be true.
+ */
+ autoPrestigeMaxRunestonesOnly?: boolean;
}
export type { PrestigeData };