generated from nhcarrigan/template
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:
@@ -14,7 +14,7 @@ A running list of planned features and content additions. Strike through items a
|
|||||||
|
|
||||||
- [x] **Boss first-kill bounties** — Defeating a boss for the very first time grants a one-time runestone bonus. Rewards exploration and makes conquering a new zone feel extra satisfying.
|
- [x] **Boss first-kill bounties** — Defeating a boss for the very first time grants a one-time runestone bonus. Rewards exploration and makes conquering a new zone feel extra satisfying.
|
||||||
|
|
||||||
- [ ] **Auto-prestige toggle** — Unlockable via the prestige shop. Automatically prestiges the moment the threshold is reached. Late-game convenience that dedicated players will love.
|
- [x] **Auto-prestige toggle** — Unlockable via the prestige shop. Automatically prestiges the moment the threshold is reached. Late-game convenience that dedicated players will love.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ A running list of planned features and content additions. Strike through items a
|
|||||||
|
|
||||||
- [x] **Statistics panel** — All-time totals: gold earned across all runs, total prestiges, bosses defeated, quests completed, time played. Idle game players love seeing big numbers about their big numbers.
|
- [x] **Statistics panel** — All-time totals: gold earned across all runs, total prestiges, bosses defeated, quests completed, time played. Idle game players love seeing big numbers about their big numbers.
|
||||||
|
|
||||||
- [ ] **Last cloud save date + Force cloud save button** — Display when the last cloud save occurred (always visible, e.g. in the ResourceBar). Include a manual "Force Save" button for peace of mind.
|
- [x] **Last cloud save date + Force cloud save button** — Display when the last cloud save occurred (always visible, e.g. in the ResourceBar). Include a manual "Force Save" button for peace of mind.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -44,6 +44,6 @@ A running list of planned features and content additions. Strike through items a
|
|||||||
4. ~~Boss first-kill bounties~~ ✅
|
4. ~~Boss first-kill bounties~~ ✅
|
||||||
5. ~~Milestone prestige bonuses~~ ✅
|
5. ~~Milestone prestige bonuses~~ ✅
|
||||||
6. ~~Equipment set bonuses~~ ✅
|
6. ~~Equipment set bonuses~~ ✅
|
||||||
7. Auto-prestige toggle (prestige shop upgrade)
|
7. ~~Auto-prestige toggle~~ ✅
|
||||||
8. The Codex / Lore Book (flavour, lower priority)
|
8. The Codex / Lore Book (flavour, lower priority)
|
||||||
9. Second prestige layer / Transcendence (big feature, save for later)
|
9. Second prestige layer / Transcendence (big feature, save for later)
|
||||||
|
|||||||
@@ -184,6 +184,16 @@ export const DEFAULT_PRESTIGE_UPGRADES: PrestigeUpgrade[] = [
|
|||||||
runestonesCost: 1_200,
|
runestonesCost: 1_200,
|
||||||
multiplier: 25,
|
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-Upgrade ────────────────────────────────────────────────
|
// ── Runestone Meta-Upgrade ────────────────────────────────────────────────
|
||||||
{
|
{
|
||||||
id: "runestone_gain_1",
|
id: "runestone_gain_1",
|
||||||
|
|||||||
@@ -101,6 +101,9 @@ export const buildPostPrestigeState = (
|
|||||||
purchasedUpgradeIds,
|
purchasedUpgradeIds,
|
||||||
lastPrestigedAt: Date.now(),
|
lastPrestigedAt: Date.now(),
|
||||||
...computeRunestoneMultipliers(purchasedUpgradeIds),
|
...computeRunestoneMultipliers(purchasedUpgradeIds),
|
||||||
|
...(currentState.prestige.autoPrestigeEnabled !== undefined
|
||||||
|
? { autoPrestigeEnabled: currentState.prestige.autoPrestigeEnabled }
|
||||||
|
: {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const freshState = INITIAL_GAME_STATE(currentState.player, characterName);
|
const freshState = INITIAL_GAME_STATE(currentState.player, characterName);
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ const HOW_TO_PLAY = [
|
|||||||
title: "🔮 Runestones & Prestige Upgrades",
|
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.",
|
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",
|
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.",
|
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.",
|
||||||
|
|||||||
@@ -36,10 +36,11 @@ const CATEGORY_ORDER: PrestigeUpgradeCategory[] = [
|
|||||||
"essence",
|
"essence",
|
||||||
"crystals",
|
"crystals",
|
||||||
"runestones",
|
"runestones",
|
||||||
|
"utility",
|
||||||
];
|
];
|
||||||
|
|
||||||
export const PrestigePanel = (): React.JSX.Element => {
|
export const PrestigePanel = (): React.JSX.Element => {
|
||||||
const { state, reload, formatNumber, buyPrestigeUpgrade } = useGame();
|
const { state, reload, formatNumber, buyPrestigeUpgrade, toggleAutoPrestige } = useGame();
|
||||||
const [characterName, setCharacterName] = useState("");
|
const [characterName, setCharacterName] = useState("");
|
||||||
const [isPending, setIsPending] = useState(false);
|
const [isPending, setIsPending] = useState(false);
|
||||||
const [result, setResult] = useState<{ runestones: number; count: number; milestoneRunestones: number } | null>(null);
|
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 canAfford = prestigeData.runestones >= upgrade.runestonesCost;
|
||||||
const isLoading = buyingId === upgrade.id;
|
const isLoading = buyingId === upgrade.id;
|
||||||
|
|
||||||
|
const isAutoPrestigeToggle = upgrade.id === "auto_prestige" && purchased;
|
||||||
|
const autoPrestigeEnabled = prestigeData.autoPrestigeEnabled ?? false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={upgrade.id}
|
key={upgrade.id}
|
||||||
@@ -217,6 +221,15 @@ export const PrestigePanel = (): React.JSX.Element => {
|
|||||||
{purchased ? "✅ Purchased" : `🔮 ${formatNumber(upgrade.runestonesCost)} Runestones`}
|
{purchased ? "✅ Purchased" : `🔮 ${formatNumber(upgrade.runestonesCost)} Runestones`}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
{isAutoPrestigeToggle && (
|
||||||
|
<button
|
||||||
|
className={`auto-prestige-toggle ${autoPrestigeEnabled ? "enabled" : "disabled"}`}
|
||||||
|
onClick={() => { toggleAutoPrestige(); }}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{autoPrestigeEnabled ? "⚡ Auto ON" : "⏸ Auto OFF"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
{!purchased && (
|
{!purchased && (
|
||||||
<button
|
<button
|
||||||
className="buy-upgrade-button"
|
className="buy-upgrade-button"
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
buyPrestigeUpgrade as buyPrestigeUpgradeApi,
|
buyPrestigeUpgrade as buyPrestigeUpgradeApi,
|
||||||
challengeBoss as challengeBossApi,
|
challengeBoss as challengeBossApi,
|
||||||
loadGame,
|
loadGame,
|
||||||
|
prestige as prestigeApi,
|
||||||
saveGame,
|
saveGame,
|
||||||
} from "../api/client.js";
|
} from "../api/client.js";
|
||||||
import { RESOURCE_CAP, applyTick, calculateClickPower } from "../engine/tick.js";
|
import { RESOURCE_CAP, applyTick, calculateClickPower } from "../engine/tick.js";
|
||||||
@@ -73,11 +74,15 @@ interface GameContextValue {
|
|||||||
formatNumber: (value: number) => string;
|
formatNumber: (value: number) => string;
|
||||||
/** Buy a prestige upgrade from the runestone shop */
|
/** Buy a prestige upgrade from the runestone shop */
|
||||||
buyPrestigeUpgrade: (upgradeId: string) => Promise<void>;
|
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 GameContext = createContext<GameContextValue | null>(null);
|
||||||
|
|
||||||
const AUTO_SAVE_INTERVAL_MS = 30_000;
|
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 => {
|
export const GameProvider = ({ children }: { children: React.ReactNode }): React.JSX.Element => {
|
||||||
const [state, setState] = useState<GameState | null>(null);
|
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 rafRef = useRef<number | null>(null);
|
||||||
const newlyUnlockedRef = useRef<Achievement[]>([]);
|
const newlyUnlockedRef = useRef<Achievement[]>([]);
|
||||||
const signatureRef = useRef<string | null>(localStorage.getItem("elysium_save_signature"));
|
const signatureRef = useRef<string | null>(localStorage.getItem("elysium_save_signature"));
|
||||||
|
const isAutoPrestigingRef = useRef(false);
|
||||||
|
const reloadRef = useRef<() => Promise<void>>(() => Promise.resolve());
|
||||||
|
|
||||||
stateRef.current = state;
|
stateRef.current = state;
|
||||||
|
|
||||||
@@ -136,6 +143,8 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
reloadRef.current = reload;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void reload();
|
void reload();
|
||||||
}, [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);
|
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) => {
|
const challengeBoss = useCallback(async (bossId: string) => {
|
||||||
if (!stateRef.current) return;
|
if (!stateRef.current) return;
|
||||||
const boss = stateRef.current.bosses.find((b) => b.id === bossId);
|
const boss = stateRef.current.bosses.find((b) => b.id === bossId);
|
||||||
@@ -566,6 +605,7 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
|
|||||||
setNumberFormat,
|
setNumberFormat,
|
||||||
formatNumber: boundFormatNumber,
|
formatNumber: boundFormatNumber,
|
||||||
buyPrestigeUpgrade,
|
buyPrestigeUpgrade,
|
||||||
|
toggleAutoPrestige,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -181,6 +181,16 @@ export const PRESTIGE_UPGRADES: PrestigeUpgrade[] = [
|
|||||||
runestonesCost: 1_200,
|
runestonesCost: 1_200,
|
||||||
multiplier: 25,
|
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 ───────────────────────────────────────────────
|
// ── Runestone Meta-Upgrades ───────────────────────────────────────────────
|
||||||
{
|
{
|
||||||
id: "runestone_gain_1",
|
id: "runestone_gain_1",
|
||||||
@@ -206,4 +216,5 @@ export const PRESTIGE_UPGRADE_CATEGORY_LABELS: Record<string, string> = {
|
|||||||
essence: "✨ Essence Production",
|
essence: "✨ Essence Production",
|
||||||
crystals: "💎 Crystal Rewards",
|
crystals: "💎 Crystal Rewards",
|
||||||
runestones: "🔮 Runestone Gain",
|
runestones: "🔮 Runestone Gain",
|
||||||
|
utility: "⚙️ Utility",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -642,6 +642,32 @@ body {
|
|||||||
font-size: 0.8rem;
|
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 ===================== */
|
||||||
.login-page {
|
.login-page {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -17,4 +17,6 @@ export interface PrestigeData {
|
|||||||
runestonesEssenceMultiplier?: number;
|
runestonesEssenceMultiplier?: number;
|
||||||
/** Pre-computed multiplier from "crystals" runestone upgrades */
|
/** Pre-computed multiplier from "crystals" runestone upgrades */
|
||||||
runestonesCrystalMultiplier?: number;
|
runestonesCrystalMultiplier?: number;
|
||||||
|
/** Whether the auto-prestige feature is currently enabled (requires auto_prestige upgrade) */
|
||||||
|
autoPrestigeEnabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ export type PrestigeUpgradeCategory =
|
|||||||
| "click"
|
| "click"
|
||||||
| "essence"
|
| "essence"
|
||||||
| "crystals"
|
| "crystals"
|
||||||
| "runestones";
|
| "runestones"
|
||||||
|
| "utility";
|
||||||
|
|
||||||
export interface PrestigeUpgrade {
|
export interface PrestigeUpgrade {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user