feat: add auto-prestige toggle via runestone shop upgrade

Adds a new Utility category to the prestige shop with the "Autonomous Ascension" upgrade (100 runestones). Once purchased, a toggle button appears in the shop that enables automatic prestige — the client RAF loop checks the threshold on each tick and fires the prestige API automatically using the current character name.

 This feature was implemented with help from Hikari~ 🌸
This commit is contained in:
2026-03-07 00:15:10 -08:00
committed by Naomi Carrigan
parent acda4c2fc4
commit b27454669e
10 changed files with 115 additions and 5 deletions
@@ -39,6 +39,10 @@ const HOW_TO_PLAY = [
title: "🔮 Runestones & Prestige Upgrades",
body: "Spend runestones in the Prestige Shop on permanent upgrades that carry over across all future runs. These upgrades multiply income, click power, essence, and crystal gain — making each new run more powerful than the last.",
},
{
title: "⚙️ Auto-Prestige",
body: "Purchase the Autonomous Ascension upgrade in the Prestige Shop (100 runestones) to unlock the Auto-Prestige toggle. When enabled, you will automatically ascend the moment you reach the prestige threshold, using your current character name. Toggle it on and off freely from the Prestige Shop.",
},
{
title: "🏆 Achievements",
body: "Earn achievements by hitting milestones — total gold earned, bosses defeated, quests completed, and more. Achievements are purely cosmetic and track your long-term progress across all prestige runs.",
+14 -1
View File
@@ -36,10 +36,11 @@ const CATEGORY_ORDER: PrestigeUpgradeCategory[] = [
"essence",
"crystals",
"runestones",
"utility",
];
export const PrestigePanel = (): React.JSX.Element => {
const { state, reload, formatNumber, buyPrestigeUpgrade } = useGame();
const { state, reload, formatNumber, buyPrestigeUpgrade, toggleAutoPrestige } = useGame();
const [characterName, setCharacterName] = useState("");
const [isPending, setIsPending] = useState(false);
const [result, setResult] = useState<{ runestones: number; count: number; milestoneRunestones: number } | null>(null);
@@ -205,6 +206,9 @@ export const PrestigePanel = (): React.JSX.Element => {
const canAfford = prestigeData.runestones >= upgrade.runestonesCost;
const isLoading = buyingId === upgrade.id;
const isAutoPrestigeToggle = upgrade.id === "auto_prestige" && purchased;
const autoPrestigeEnabled = prestigeData.autoPrestigeEnabled ?? false;
return (
<div
key={upgrade.id}
@@ -217,6 +221,15 @@ export const PrestigePanel = (): React.JSX.Element => {
{purchased ? "✅ Purchased" : `🔮 ${formatNumber(upgrade.runestonesCost)} Runestones`}
</p>
</div>
{isAutoPrestigeToggle && (
<button
className={`auto-prestige-toggle ${autoPrestigeEnabled ? "enabled" : "disabled"}`}
onClick={() => { toggleAutoPrestige(); }}
type="button"
>
{autoPrestigeEnabled ? "⚡ Auto ON" : "⏸ Auto OFF"}
</button>
)}
{!purchased && (
<button
className="buy-upgrade-button"
+40
View File
@@ -11,6 +11,7 @@ import {
buyPrestigeUpgrade as buyPrestigeUpgradeApi,
challengeBoss as challengeBossApi,
loadGame,
prestige as prestigeApi,
saveGame,
} from "../api/client.js";
import { RESOURCE_CAP, applyTick, calculateClickPower } from "../engine/tick.js";
@@ -73,11 +74,15 @@ interface GameContextValue {
formatNumber: (value: number) => string;
/** Buy a prestige upgrade from the runestone shop */
buyPrestigeUpgrade: (upgradeId: string) => Promise<void>;
/** Toggle the auto-prestige setting on/off (requires auto_prestige upgrade) */
toggleAutoPrestige: () => void;
}
const GameContext = createContext<GameContextValue | null>(null);
const AUTO_SAVE_INTERVAL_MS = 30_000;
const AUTO_PRESTIGE_THRESHOLD_BASE = 1_000_000;
const AUTO_PRESTIGE_THRESHOLD_SCALE = 5;
export const GameProvider = ({ children }: { children: React.ReactNode }): React.JSX.Element => {
const [state, setState] = useState<GameState | null>(null);
@@ -98,6 +103,8 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
const rafRef = useRef<number | null>(null);
const newlyUnlockedRef = useRef<Achievement[]>([]);
const signatureRef = useRef<string | null>(localStorage.getItem("elysium_save_signature"));
const isAutoPrestigingRef = useRef(false);
const reloadRef = useRef<() => Promise<void>>(() => Promise.resolve());
stateRef.current = state;
@@ -136,6 +143,8 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
}
}, []);
reloadRef.current = reload;
useEffect(() => {
void reload();
}, [reload]);
@@ -191,6 +200,23 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
}
}
// Auto-prestige: fire when unlocked, enabled, and threshold is met
const autoState = stateRef.current;
if (
!isAutoPrestigingRef.current &&
autoState?.prestige.purchasedUpgradeIds.includes("auto_prestige") &&
autoState.prestige.autoPrestigeEnabled &&
autoState.player.totalGoldEarned >=
AUTO_PRESTIGE_THRESHOLD_BASE *
Math.pow(AUTO_PRESTIGE_THRESHOLD_SCALE, autoState.prestige.count)
) {
isAutoPrestigingRef.current = true;
void prestigeApi({ characterName: autoState.player.characterName })
.then(() => reloadRef.current())
.catch(() => { /* silently ignore — will retry next tick */ })
.finally(() => { isAutoPrestigingRef.current = false; });
}
rafRef.current = requestAnimationFrame(tick);
};
@@ -404,6 +430,19 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
}
}, []);
const toggleAutoPrestige = useCallback(() => {
setState((prev) => {
if (!prev) return prev;
return {
...prev,
prestige: {
...prev.prestige,
autoPrestigeEnabled: !prev.prestige.autoPrestigeEnabled,
},
};
});
}, []);
const challengeBoss = useCallback(async (bossId: string) => {
if (!stateRef.current) return;
const boss = stateRef.current.bosses.find((b) => b.id === bossId);
@@ -566,6 +605,7 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
setNumberFormat,
formatNumber: boundFormatNumber,
buyPrestigeUpgrade,
toggleAutoPrestige,
}}
>
{children}
+11
View File
@@ -181,6 +181,16 @@ export const PRESTIGE_UPGRADES: PrestigeUpgrade[] = [
runestonesCost: 1_200,
multiplier: 25,
},
// ── Utility Unlocks ───────────────────────────────────────────────────────
{
id: "auto_prestige",
name: "Autonomous Ascension",
description:
"Unlock the Auto-Prestige toggle. When enabled, you will automatically ascend the moment you reach the prestige threshold — using your current character name.",
category: "utility",
runestonesCost: 100,
multiplier: 1,
},
// ── Runestone Meta-Upgrades ───────────────────────────────────────────────
{
id: "runestone_gain_1",
@@ -206,4 +216,5 @@ export const PRESTIGE_UPGRADE_CATEGORY_LABELS: Record<string, string> = {
essence: "✨ Essence Production",
crystals: "💎 Crystal Rewards",
runestones: "🔮 Runestone Gain",
utility: "⚙️ Utility",
};
+26
View File
@@ -642,6 +642,32 @@ body {
font-size: 0.8rem;
}
.auto-prestige-toggle {
border-radius: var(--radius-sm);
border: 1px solid var(--colour-border);
cursor: pointer;
font-size: 0.85rem;
font-weight: 600;
padding: 0.4rem 0.8rem;
transition: background 0.2s, border-color 0.2s;
white-space: nowrap;
}
.auto-prestige-toggle.enabled {
background: var(--colour-success);
border-color: var(--colour-success);
color: #fff;
}
.auto-prestige-toggle.disabled {
background: var(--colour-surface);
color: var(--colour-text-muted);
}
.auto-prestige-toggle:hover {
border-color: var(--colour-accent);
}
/* ===================== LOGIN PAGE ===================== */
.login-page {
align-items: center;