feat: post-prestige automation (auto-adventurer) (#76)
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 1m5s
CI / Lint, Build & Test (push) Successful in 1m9s

## Summary

Closes #61

- Adds the **Autonomous Recruitment** prestige upgrade (50 runestones) to both the API and web data files
- Adds `autoAdventurer?: boolean` to the `GameState` type for backwards-compatible saves
- Adds tick-loop logic in GameContext that automatically purchases the highest-tier unlocked adventurer the player can afford each frame when the toggle is enabled
- Adds `toggleAutoAdventurer` callback and exposes it through the context
- Adds toggle UI in the Prestige Shop (mirrors the existing Auto-Prestige toggle pattern)
- Updates the How to Play guide in the About panel to document the new automation feature

 This issue was created with help from Hikari~ 🌸

Reviewed-on: #76
Co-authored-by: Hikari <hikari@nhcarrigan.com>
Co-committed-by: Hikari <hikari@nhcarrigan.com>
This commit was merged in pull request #76.
This commit is contained in:
2026-03-19 13:38:25 -07:00
committed by Naomi Carrigan
parent 911e089a9e
commit 3d114f63d7
6 changed files with 112 additions and 9 deletions
+9
View File
@@ -210,6 +210,15 @@ export const defaultPrestigeUpgrades: Array<PrestigeUpgrade> = [
runestonesCost: 1200,
},
// ── Utility Unlocks ───────────────────────────────────────────────────────
{
category: "utility",
description:
"Unlock the Auto-Adventurer toggle. When enabled, the tick engine will automatically purchase the highest-tier adventurer you can currently afford.",
id: "auto_adventurer",
multiplier: 1,
name: "Autonomous Recruitment",
runestonesCost: 50,
},
{
category: "utility",
description:
+10 -8
View File
@@ -188,14 +188,16 @@ const howToPlay = [
},
{
body:
"Toggle automation in the Quests and Boss Encounters panels! Auto-Quest"
+ " automatically sends your party on the highest-zone available quest"
+ " as soon as one completes, skipping quests whose combat power"
+ " requirement isn't met. Auto-Boss automatically challenges the"
+ " highest available boss as soon as one is ready. Both can be toggled"
+ " on or off at any time using the 🤖 Auto button in each panel"
+ " header.",
title: "🤖 Auto-Quest & Auto-Boss",
"Toggle automation in the Quests, Boss Encounters, and Prestige Shop"
+ " panels! Auto-Quest automatically sends your party on the"
+ " highest-zone available quest as soon as one completes, skipping"
+ " quests whose combat power requirement isn't met. Auto-Boss"
+ " automatically challenges the highest available boss as soon as one"
+ " is ready. Auto-Adventurer (unlocked via the Prestige Shop for 50"
+ " runestones) automatically purchases the highest-tier adventurer you"
+ " can currently afford each tick, keeping your income growing after a"
+ " prestige without any manual clicks.",
title: "🤖 Auto-Quest, Auto-Boss & Auto-Adventurer",
},
{
body:
+24 -1
View File
@@ -89,6 +89,7 @@ const PrestigePanel = (): JSX.Element => {
buyPrestigeUpgrade,
enableNotifications,
enableSounds,
toggleAutoAdventurer,
toggleAutoPrestige,
triggerPrestigeToast,
} = useGame();
@@ -110,7 +111,7 @@ const PrestigePanel = (): JSX.Element => {
);
}
const { prestige: prestigeData, player } = state;
const { autoAdventurer, prestige: prestigeData, player } = state;
const threshold = calculateThreshold(prestigeData.count);
const isEligible = player.totalGoldEarned >= threshold;
const runestonePreview = calculateRunestonePreview(
@@ -173,6 +174,10 @@ const PrestigePanel = (): JSX.Element => {
void handlePrestige();
}
function handleAutoAdventurerToggle(): void {
toggleAutoAdventurer();
}
function handleAutoPrestigeToggle(): void {
toggleAutoPrestige();
}
@@ -347,6 +352,9 @@ const PrestigePanel = (): JSX.Element => {
= prestigeData.runestones >= upgrade.runestonesCost;
const isLoading = buyingId === upgrade.id;
const isAutoAdventurerToggle
= upgrade.id === "auto_adventurer" && purchased;
const autoAdventurerEnabled = autoAdventurer ?? false;
const isAutoPrestigeToggle
= upgrade.id === "auto_prestige" && purchased;
const autoPrestigeEnabled
@@ -381,6 +389,21 @@ const PrestigePanel = (): JSX.Element => {
: `🔮 ${formatNumber(upgrade.runestonesCost)} Runestones`}
</p>
</div>
{isAutoAdventurerToggle
? <button
className={`auto-prestige-toggle ${
autoAdventurerEnabled
? "enabled"
: "disabled"
}`}
onClick={handleAutoAdventurerToggle}
type="button"
>
{autoAdventurerEnabled
? "⚡ Auto ON"
: "⏸ Auto OFF"}
</button>
: null}
{isAutoPrestigeToggle
? <button
className={`auto-prestige-toggle ${
+55
View File
@@ -447,6 +447,11 @@ interface GameContextValue {
*/
toggleAutoBoss: ()=> void;
/**
* Toggle the auto-adventurer setting on/off (requires auto_adventurer prestige upgrade).
*/
toggleAutoAdventurer: ()=> void;
/**
* Queue of newly unlocked codex entry IDs (for toast notifications).
*/
@@ -1073,6 +1078,42 @@ export const GameProvider = ({
}
}
// Auto-adventurer: buy one of the highest-tier affordable unlocked adventurer per tick
if (
next.autoAdventurer === true
&& next.prestige.purchasedUpgradeIds.includes("auto_adventurer")
) {
const [ bestAdventurer ] = next.adventurers.
filter((adventurer) => {
const cost
= adventurer.baseCost * Math.pow(1.15, adventurer.count);
return adventurer.unlocked && next.resources.gold >= cost;
}).
sort((adventurerA, adventurerB) => {
const costA
= adventurerA.baseCost * Math.pow(1.15, adventurerA.count);
const costB
= adventurerB.baseCost * Math.pow(1.15, adventurerB.count);
return costB - costA;
});
if (bestAdventurer !== undefined) {
const purchaseCost
= bestAdventurer.baseCost * Math.pow(1.15, bestAdventurer.count);
next = {
...next,
adventurers: next.adventurers.map((adventurer) => {
return adventurer.id === bestAdventurer.id
? { ...adventurer, count: adventurer.count + 1 }
: adventurer;
}),
resources: {
...next.resources,
gold: next.resources.gold - purchaseCost,
},
};
}
}
// Detect newly unlocked achievements
unlockedAchievementsReference.current = next.achievements.filter(
(a, index) => {
@@ -1858,6 +1899,18 @@ export const GameProvider = ({
});
}, []);
const toggleAutoAdventurer = useCallback(() => {
setState((previous) => {
if (previous === null) {
return previous;
}
return {
...previous,
autoAdventurer: previous.autoAdventurer !== true,
};
});
}, []);
const setActiveCompanion = useCallback((companionId: string | null) => {
setState((previous) => {
if (previous === null) {
@@ -2169,6 +2222,7 @@ export const GameProvider = ({
startQuest,
state,
syncError,
toggleAutoAdventurer,
toggleAutoBoss,
toggleAutoPrestige,
toggleAutoQuest,
@@ -2240,6 +2294,7 @@ export const GameProvider = ({
startQuest,
state,
syncError,
toggleAutoAdventurer,
toggleAutoBoss,
toggleAutoPrestige,
toggleAutoQuest,
+9
View File
@@ -212,6 +212,15 @@ export const PRESTIGE_UPGRADES: Array<PrestigeUpgrade> = [
runestonesCost: 1200,
},
// ── Utility Unlocks ───────────────────────────────────────────────────────
{
category: "utility",
description:
"Unlock the Auto-Adventurer toggle. When enabled, the tick engine will automatically purchase the highest-tier adventurer you can currently afford.",
id: "auto_adventurer",
multiplier: 1,
name: "Autonomous Recruitment",
runestonesCost: 50,
},
{
category: "utility",
description:
@@ -79,6 +79,11 @@ interface GameState {
*/
autoBoss?: boolean;
/**
* When true, the tick engine automatically purchases the highest-tier affordable adventurer.
*/
autoAdventurer?: boolean;
/**
* Companion unlock and active selection state — optional for backwards compatibility.
*/