generated from nhcarrigan/template
feat: achievement timestamps, companion progress bars, max runestone indicator, and auto-prestige max-runes mode
This commit is contained in:
@@ -189,6 +189,7 @@ const buildPostPrestigeState = (
|
|||||||
} => {
|
} => {
|
||||||
const {
|
const {
|
||||||
autoPrestigeEnabled,
|
autoPrestigeEnabled,
|
||||||
|
autoPrestigeMaxRunestonesOnly,
|
||||||
count: currentPrestigeCount,
|
count: currentPrestigeCount,
|
||||||
purchasedUpgradeIds,
|
purchasedUpgradeIds,
|
||||||
runestones: currentRunestones,
|
runestones: currentRunestones,
|
||||||
@@ -215,6 +216,9 @@ const buildPostPrestigeState = (
|
|||||||
...autoPrestigeEnabled === undefined
|
...autoPrestigeEnabled === undefined
|
||||||
? {}
|
? {}
|
||||||
: { autoPrestigeEnabled },
|
: { autoPrestigeEnabled },
|
||||||
|
...autoPrestigeMaxRunestonesOnly === undefined
|
||||||
|
? {}
|
||||||
|
: { autoPrestigeMaxRunestonesOnly },
|
||||||
};
|
};
|
||||||
|
|
||||||
const freshState = initialGameState(currentState.player, characterName);
|
const freshState = initialGameState(currentState.player, characterName);
|
||||||
|
|||||||
@@ -255,6 +255,20 @@ describe("buildPostPrestigeState", () => {
|
|||||||
expect(prestigeData.autoPrestigeEnabled).toBeUndefined();
|
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", () => {
|
it("preserves apotheosis data across prestige", () => {
|
||||||
const apotheosis = { count: 2 };
|
const apotheosis = { count: 2 };
|
||||||
const state = makeMinimalState({ apotheosis });
|
const state = makeMinimalState({ apotheosis });
|
||||||
|
|||||||
@@ -156,7 +156,18 @@ const AchievementCard = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="achievement-status">
|
<div className="achievement-status">
|
||||||
{isUnlocked
|
{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>
|
: <span className="achievement-locked-badge">{"🔒"}</span>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -30,11 +30,12 @@ const unlockLabels: Record<string, string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface CompanionCardProperties {
|
interface CompanionCardProperties {
|
||||||
readonly companion: Companion;
|
readonly companion: Companion;
|
||||||
readonly isUnlocked: boolean;
|
readonly isUnlocked: boolean;
|
||||||
readonly isActive: boolean;
|
readonly isActive: boolean;
|
||||||
readonly onSelect: ()=> void;
|
readonly onSelect: ()=> void;
|
||||||
readonly formatNumber: (n: number)=> string;
|
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.isActive - Whether this companion is currently active.
|
||||||
* @param props.onSelect - Callback when the companion is selected/deselected.
|
* @param props.onSelect - Callback when the companion is selected/deselected.
|
||||||
* @param props.formatNumber - The number formatting utility function.
|
* @param props.formatNumber - The number formatting utility function.
|
||||||
|
* @param props.currentProgress - The player's current progress toward the unlock threshold.
|
||||||
* @returns The JSX element.
|
* @returns The JSX element.
|
||||||
*/
|
*/
|
||||||
const CompanionCard = ({
|
const CompanionCard = ({
|
||||||
@@ -53,6 +55,7 @@ const CompanionCard = ({
|
|||||||
isActive,
|
isActive,
|
||||||
onSelect,
|
onSelect,
|
||||||
formatNumber,
|
formatNumber,
|
||||||
|
currentProgress,
|
||||||
}: CompanionCardProperties): JSX.Element => {
|
}: CompanionCardProperties): JSX.Element => {
|
||||||
const bonusSign = companion.bonus.type === "questTime"
|
const bonusSign = companion.bonus.type === "questTime"
|
||||||
? "-"
|
? "-"
|
||||||
@@ -111,11 +114,28 @@ const CompanionCard = ({
|
|||||||
: "Activate"}
|
: "Activate"}
|
||||||
</button>
|
</button>
|
||||||
: <div className="companion-unlock-requirement">
|
: <div className="companion-unlock-requirement">
|
||||||
{"🔒 Unlock: "}
|
<p>
|
||||||
{companion.unlock.type === "lifetimeGold"
|
{"🔒 Unlock: "}
|
||||||
? formatNumber(companion.unlock.threshold)
|
{companion.unlock.type === "lifetimeGold"
|
||||||
: String(companion.unlock.threshold)}{" "}
|
? formatNumber(companion.unlock.threshold)
|
||||||
{unlockLabels[companion.unlock.type] ?? companion.unlock.type}
|
: 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>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -140,6 +160,15 @@ const CompanionPanel = (): JSX.Element => {
|
|||||||
const unlockedIds = state.companions?.unlockedCompanionIds ?? [];
|
const unlockedIds = state.companions?.unlockedCompanionIds ?? [];
|
||||||
const activeId = state.companions?.activeCompanionId ?? null;
|
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 {
|
function handleSelect(companionId: string): void {
|
||||||
setActiveCompanion(activeId === companionId
|
setActiveCompanion(activeId === companionId
|
||||||
? null
|
? null
|
||||||
@@ -177,6 +206,9 @@ const CompanionPanel = (): JSX.Element => {
|
|||||||
return (
|
return (
|
||||||
<CompanionCard
|
<CompanionCard
|
||||||
companion={companion}
|
companion={companion}
|
||||||
|
currentProgress={
|
||||||
|
progressByUnlockType[companion.unlock.type] ?? 0
|
||||||
|
}
|
||||||
formatNumber={formatNumber}
|
formatNumber={formatNumber}
|
||||||
isActive={activeId === companion.id}
|
isActive={activeId === companion.id}
|
||||||
isUnlocked={unlockedIds.includes(companion.id)}
|
isUnlocked={unlockedIds.includes(companion.id)}
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ const PrestigePanel = (): JSX.Element => {
|
|||||||
enableSounds,
|
enableSounds,
|
||||||
toggleAutoAdventurer,
|
toggleAutoAdventurer,
|
||||||
toggleAutoPrestige,
|
toggleAutoPrestige,
|
||||||
|
toggleAutoPrestigeMaxRunestones,
|
||||||
triggerPrestigeToast,
|
triggerPrestigeToast,
|
||||||
} = useGame();
|
} = useGame();
|
||||||
const [ isPending, setIsPending ] = useState(false);
|
const [ isPending, setIsPending ] = useState(false);
|
||||||
@@ -93,6 +94,11 @@ const PrestigePanel = (): JSX.Element => {
|
|||||||
const isEligible = player.totalGoldEarned >= threshold;
|
const isEligible = player.totalGoldEarned >= threshold;
|
||||||
const runestonePreview = computeProjectedRunestones(state);
|
const runestonePreview = computeProjectedRunestones(state);
|
||||||
const nextMultiplier = calculateProductionMultiplier(prestigeData.count + 1);
|
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> {
|
async function handlePrestige(): Promise<void> {
|
||||||
setIsPending(true);
|
setIsPending(true);
|
||||||
@@ -155,6 +161,10 @@ const PrestigePanel = (): JSX.Element => {
|
|||||||
toggleAutoPrestige();
|
toggleAutoPrestige();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleAutoPrestigeMaxRunestonesToggle(): void {
|
||||||
|
toggleAutoPrestigeMaxRunestones();
|
||||||
|
}
|
||||||
|
|
||||||
function handlePrestigeTabClick(): void {
|
function handlePrestigeTabClick(): void {
|
||||||
setActiveTab("prestige");
|
setActiveTab("prestige");
|
||||||
}
|
}
|
||||||
@@ -241,6 +251,16 @@ const PrestigePanel = (): JSX.Element => {
|
|||||||
{"+"}
|
{"+"}
|
||||||
{formatInteger(runestonePreview)}
|
{formatInteger(runestonePreview)}
|
||||||
</strong>
|
</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>
|
</p>
|
||||||
: null}
|
: null}
|
||||||
{isEligible
|
{isEligible
|
||||||
@@ -332,6 +352,8 @@ const PrestigePanel = (): JSX.Element => {
|
|||||||
= upgrade.id === "auto_prestige" && purchased;
|
= upgrade.id === "auto_prestige" && purchased;
|
||||||
const autoPrestigeEnabled
|
const autoPrestigeEnabled
|
||||||
= prestigeData.autoPrestigeEnabled ?? false;
|
= prestigeData.autoPrestigeEnabled ?? false;
|
||||||
|
const autoPrestigeMaxRunestonesOnly
|
||||||
|
= prestigeData.autoPrestigeMaxRunestonesOnly ?? false;
|
||||||
|
|
||||||
function handleBuyClick(): void {
|
function handleBuyClick(): void {
|
||||||
void handleBuyUpgrade(upgrade.id);
|
void handleBuyUpgrade(upgrade.id);
|
||||||
@@ -378,19 +400,37 @@ const PrestigePanel = (): JSX.Element => {
|
|||||||
</button>
|
</button>
|
||||||
: null}
|
: null}
|
||||||
{isAutoPrestigeToggle
|
{isAutoPrestigeToggle
|
||||||
? <button
|
? <>
|
||||||
className={`auto-prestige-toggle ${
|
<button
|
||||||
autoPrestigeEnabled
|
className={`auto-prestige-toggle ${
|
||||||
? "enabled"
|
autoPrestigeEnabled
|
||||||
: "disabled"
|
? "enabled"
|
||||||
}`}
|
: "disabled"
|
||||||
onClick={handleAutoPrestigeToggle}
|
}`}
|
||||||
type="button"
|
onClick={handleAutoPrestigeToggle}
|
||||||
>
|
type="button"
|
||||||
|
>
|
||||||
|
{autoPrestigeEnabled
|
||||||
|
? "⚡ Auto ON"
|
||||||
|
: "⏸ Auto OFF"}
|
||||||
|
</button>
|
||||||
{autoPrestigeEnabled
|
{autoPrestigeEnabled
|
||||||
? "⚡ Auto ON"
|
? <button
|
||||||
: "⏸ Auto OFF"}
|
className={`auto-prestige-toggle ${
|
||||||
</button>
|
autoPrestigeMaxRunestonesOnly
|
||||||
|
? "enabled"
|
||||||
|
: "disabled"
|
||||||
|
}`}
|
||||||
|
onClick={handleAutoPrestigeMaxRunestonesToggle}
|
||||||
|
title="Only fire at max runestone yield"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{autoPrestigeMaxRunestonesOnly
|
||||||
|
? "⚡ Max Runes Only"
|
||||||
|
: "⏸ Max Runes OFF"}
|
||||||
|
</button>
|
||||||
|
: null}
|
||||||
|
</>
|
||||||
: null}
|
: null}
|
||||||
{purchased
|
{purchased
|
||||||
? null
|
? null
|
||||||
|
|||||||
@@ -479,6 +479,11 @@ interface GameContextValue {
|
|||||||
*/
|
*/
|
||||||
toggleAutoPrestige: ()=> void;
|
toggleAutoPrestige: ()=> void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle whether auto-prestige waits for maximum runestone yield before firing.
|
||||||
|
*/
|
||||||
|
toggleAutoPrestigeMaxRunestones: ()=> void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggle the auto-quest setting on/off.
|
* Toggle the auto-quest setting on/off.
|
||||||
*/
|
*/
|
||||||
@@ -1391,15 +1396,27 @@ export const GameProvider = ({
|
|||||||
|
|
||||||
// Auto-prestige: fire when unlocked, enabled, and threshold is met
|
// Auto-prestige: fire when unlocked, enabled, and threshold is met
|
||||||
const autoState = stateReference.current;
|
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 (
|
if (
|
||||||
!isAutoPrestigingReference.current
|
!isAutoPrestigingReference.current
|
||||||
&& autoState?.prestige.purchasedUpgradeIds.includes("auto_prestige")
|
&& autoState?.prestige.purchasedUpgradeIds.includes("auto_prestige")
|
||||||
=== true
|
=== true
|
||||||
&& autoState.prestige.autoPrestigeEnabled === true
|
&& autoState.prestige.autoPrestigeEnabled === true
|
||||||
&& autoState.player.totalGoldEarned
|
&& autoState.player.totalGoldEarned >= autoPrestigeThreshold
|
||||||
>= autoPrestigeThresholdBase
|
&& autoMaxRunestonesMet
|
||||||
* Math.pow(autoState.prestige.count + 1, 2.5)
|
|
||||||
* (autoState.transcendence?.echoPrestigeThresholdMultiplier ?? 1)
|
|
||||||
) {
|
) {
|
||||||
isAutoPrestigingReference.current = true;
|
isAutoPrestigingReference.current = true;
|
||||||
void prestigeApi({}).
|
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(() => {
|
const toggleAutoQuest = useCallback(() => {
|
||||||
setState((previous) => {
|
setState((previous) => {
|
||||||
if (previous === null) {
|
if (previous === null) {
|
||||||
@@ -2496,6 +2529,7 @@ export const GameProvider = ({
|
|||||||
toggleAutoAdventurer,
|
toggleAutoAdventurer,
|
||||||
toggleAutoBoss,
|
toggleAutoBoss,
|
||||||
toggleAutoPrestige,
|
toggleAutoPrestige,
|
||||||
|
toggleAutoPrestigeMaxRunestones,
|
||||||
toggleAutoQuest,
|
toggleAutoQuest,
|
||||||
transcend,
|
transcend,
|
||||||
triggerPrestigeToast,
|
triggerPrestigeToast,
|
||||||
@@ -2571,6 +2605,7 @@ export const GameProvider = ({
|
|||||||
toggleAutoAdventurer,
|
toggleAutoAdventurer,
|
||||||
toggleAutoBoss,
|
toggleAutoBoss,
|
||||||
toggleAutoPrestige,
|
toggleAutoPrestige,
|
||||||
|
toggleAutoPrestigeMaxRunestones,
|
||||||
toggleAutoQuest,
|
toggleAutoQuest,
|
||||||
transcend,
|
transcend,
|
||||||
triggerPrestigeToast,
|
triggerPrestigeToast,
|
||||||
|
|||||||
@@ -56,6 +56,12 @@ interface PrestigeData {
|
|||||||
* Whether the auto-prestige feature is currently enabled (requires auto_prestige upgrade).
|
* Whether the auto-prestige feature is currently enabled (requires auto_prestige upgrade).
|
||||||
*/
|
*/
|
||||||
autoPrestigeEnabled?: boolean;
|
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 };
|
export type { PrestigeData };
|
||||||
|
|||||||
Reference in New Issue
Block a user