generated from nhcarrigan/template
feat: add auto-quest and auto-boss toggles
Adds optional automation to the quest and boss panels. Auto-quest automatically starts the highest-zone available quest (respecting CP requirements) as soon as none is active. Auto-boss automatically challenges the highest available boss when one is ready. Both run exclusively in the client-side RAF tick loop — offline calculations are unaffected. Toggles persist in GameState via cloud save.
This commit is contained in:
@@ -83,6 +83,10 @@ const HOW_TO_PLAY = [
|
|||||||
title: "🔥 Daily Login Bonus",
|
title: "🔥 Daily Login Bonus",
|
||||||
body: "Log in every day to earn escalating rewards! Each consecutive day awards more gold, and the 7th day of your streak grants bonus crystals. Your streak resets if you miss a day. A week multiplier increases all rewards the longer your overall streak runs. Your current streak is displayed on your character sheet.",
|
body: "Log in every day to earn escalating rewards! Each consecutive day awards more gold, and the 7th day of your streak grants bonus crystals. Your streak resets if you miss a day. A week multiplier increases all rewards the longer your overall streak runs. Your current streak is displayed on your character sheet.",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "🤖 Auto-Quest & Auto-Boss",
|
||||||
|
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: "☁️ Cloud Saves",
|
title: "☁️ Cloud Saves",
|
||||||
body: "Your progress is automatically saved to the cloud every 30 seconds whilst you play. You can also force a manual save at any time using the sync button in the resource bar. Your save is protected by HMAC validation to ensure data integrity.",
|
body: "Your progress is automatically saved to the cloud every 30 seconds whilst you play. You can also force a manual save at any time using the sync button in the resource bar. Your save is protected by HMAC validation to ensure data integrity.",
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ const BossCard = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const BossPanel = (): React.JSX.Element => {
|
export const BossPanel = (): React.JSX.Element => {
|
||||||
const { state, challengeBoss, formatNumber } = useGame();
|
const { state, challengeBoss, formatNumber, toggleAutoBoss } = useGame();
|
||||||
const [challengingBossId, setChallengingBossId] = useState<string | null>(null);
|
const [challengingBossId, setChallengingBossId] = useState<string | null>(null);
|
||||||
const [activeZoneId, setActiveZoneId] = useState("verdant_vale");
|
const [activeZoneId, setActiveZoneId] = useState("verdant_vale");
|
||||||
const [showLocked, setShowLocked] = useState(true);
|
const [showLocked, setShowLocked] = useState(true);
|
||||||
@@ -187,11 +187,21 @@ export const BossPanel = (): React.JSX.Element => {
|
|||||||
<section className="panel boss-panel">
|
<section className="panel boss-panel">
|
||||||
<div className="panel-header">
|
<div className="panel-header">
|
||||||
<h2>Boss Encounters</h2>
|
<h2>Boss Encounters</h2>
|
||||||
<LockToggle
|
<div className="panel-header-controls">
|
||||||
lockedCount={lockedCount}
|
<button
|
||||||
showLocked={showLocked}
|
className={`auto-toggle-btn ${state.autoBoss ? "auto-toggle-on" : "auto-toggle-off"}`}
|
||||||
onToggle={() => { setShowLocked((v) => !v); }}
|
onClick={toggleAutoBoss}
|
||||||
/>
|
title="Automatically challenge the highest available boss"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
🤖 Auto: {state.autoBoss ? "ON" : "OFF"}
|
||||||
|
</button>
|
||||||
|
<LockToggle
|
||||||
|
lockedCount={lockedCount}
|
||||||
|
showLocked={showLocked}
|
||||||
|
onToggle={() => { setShowLocked((v) => !v); }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ZoneSelector
|
<ZoneSelector
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ const QuestCard = ({ quest, partyCombatPower, unlockHint, zoneHint }: QuestCardP
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const QuestPanel = (): React.JSX.Element => {
|
export const QuestPanel = (): React.JSX.Element => {
|
||||||
const { state } = useGame();
|
const { state, toggleAutoQuest } = useGame();
|
||||||
const [activeZoneId, setActiveZoneId] = useState("verdant_vale");
|
const [activeZoneId, setActiveZoneId] = useState("verdant_vale");
|
||||||
const [showLocked, setShowLocked] = useState(true);
|
const [showLocked, setShowLocked] = useState(true);
|
||||||
|
|
||||||
@@ -128,11 +128,21 @@ export const QuestPanel = (): React.JSX.Element => {
|
|||||||
<section className="panel quest-panel">
|
<section className="panel quest-panel">
|
||||||
<div className="panel-header">
|
<div className="panel-header">
|
||||||
<h2>Quests</h2>
|
<h2>Quests</h2>
|
||||||
<LockToggle
|
<div className="panel-header-controls">
|
||||||
lockedCount={lockedCount}
|
<button
|
||||||
showLocked={showLocked}
|
className={`auto-toggle-btn ${state.autoQuest ? "auto-toggle-on" : "auto-toggle-off"}`}
|
||||||
onToggle={() => { setShowLocked((v) => !v); }}
|
onClick={toggleAutoQuest}
|
||||||
/>
|
title="Automatically send the party on the highest available quest"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
🤖 Auto: {state.autoQuest ? "ON" : "OFF"}
|
||||||
|
</button>
|
||||||
|
<LockToggle
|
||||||
|
lockedCount={lockedCount}
|
||||||
|
showLocked={showLocked}
|
||||||
|
onToggle={() => { setShowLocked((v) => !v); }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ZoneSelector
|
<ZoneSelector
|
||||||
|
|||||||
@@ -1,4 +1,91 @@
|
|||||||
import type { Achievement, BossChallengeResponse, ExploreCollectResponse, GameState, LoginBonusResult, NumberFormat } from "@elysium/types";
|
import type { Achievement, BossChallengeResponse, ExploreCollectResponse, GameState, LoginBonusResult, NumberFormat } from "@elysium/types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pure function — applies a boss challenge result to the game state.
|
||||||
|
* Used by both the manual challengeBoss flow and the auto-boss tick logic.
|
||||||
|
*/
|
||||||
|
const applyBossResult = (prev: GameState, bossId: string, result: BossChallengeResponse): GameState => {
|
||||||
|
if (result.won) {
|
||||||
|
const defeatedBoss = prev.bosses.find((b) => b.id === bossId);
|
||||||
|
const zoneBosses = prev.bosses.filter((b) => b.zoneId === defeatedBoss?.zoneId);
|
||||||
|
const zoneIdx = zoneBosses.findIndex((b) => b.id === bossId);
|
||||||
|
const nextZoneBossId = zoneBosses[zoneIdx + 1]?.id;
|
||||||
|
|
||||||
|
const newlyUnlockedZones = (prev.zones ?? []).filter((z) => {
|
||||||
|
if (z.status !== "locked" || z.unlockBossId !== bossId) return false;
|
||||||
|
const questOk =
|
||||||
|
z.unlockQuestId == null ||
|
||||||
|
prev.quests.some((q) => q.id === z.unlockQuestId && q.status === "completed");
|
||||||
|
return questOk;
|
||||||
|
});
|
||||||
|
const newZoneFirstBossIds = newlyUnlockedZones.map((z) => {
|
||||||
|
const firstBoss = prev.bosses.find((b) => b.zoneId === z.id);
|
||||||
|
return firstBoss?.id;
|
||||||
|
}).filter(Boolean);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
bosses: prev.bosses.map((b) => {
|
||||||
|
if (b.id === bossId) return { ...b, status: "defeated" as const, currentHp: 0 };
|
||||||
|
if (b.id === nextZoneBossId && b.prestigeRequirement <= prev.prestige.count) {
|
||||||
|
return { ...b, status: "available" as const };
|
||||||
|
}
|
||||||
|
if (newZoneFirstBossIds.includes(b.id) && b.prestigeRequirement <= prev.prestige.count) {
|
||||||
|
return { ...b, status: "available" as const };
|
||||||
|
}
|
||||||
|
return b;
|
||||||
|
}),
|
||||||
|
zones: (prev.zones ?? []).map((z) => {
|
||||||
|
if (z.status !== "locked" || z.unlockBossId !== bossId) return z;
|
||||||
|
const questOk =
|
||||||
|
z.unlockQuestId == null ||
|
||||||
|
prev.quests.some((q) => q.id === z.unlockQuestId && q.status === "completed");
|
||||||
|
return questOk ? { ...z, status: "unlocked" as const } : z;
|
||||||
|
}),
|
||||||
|
resources: result.rewards
|
||||||
|
? {
|
||||||
|
...prev.resources,
|
||||||
|
gold: prev.resources.gold + result.rewards.gold,
|
||||||
|
essence: prev.resources.essence + result.rewards.essence,
|
||||||
|
crystals: prev.resources.crystals + result.rewards.crystals,
|
||||||
|
}
|
||||||
|
: prev.resources,
|
||||||
|
prestige: result.rewards?.bountyRunestones
|
||||||
|
? { ...prev.prestige, runestones: prev.prestige.runestones + result.rewards.bountyRunestones }
|
||||||
|
: prev.prestige,
|
||||||
|
player: result.rewards
|
||||||
|
? { ...prev.player, totalGoldEarned: prev.player.totalGoldEarned + result.rewards.gold }
|
||||||
|
: prev.player,
|
||||||
|
upgrades: result.rewards
|
||||||
|
? prev.upgrades.map((u) =>
|
||||||
|
result.rewards!.upgradeIds.includes(u.id) ? { ...u, unlocked: true } : u,
|
||||||
|
)
|
||||||
|
: prev.upgrades,
|
||||||
|
equipment: result.rewards
|
||||||
|
? (prev.equipment ?? []).map((e) => {
|
||||||
|
if (!result.rewards!.equipmentIds.includes(e.id)) return e;
|
||||||
|
const slotEmpty = !(prev.equipment ?? []).some(
|
||||||
|
(other) => other.type === e.type && other.equipped,
|
||||||
|
);
|
||||||
|
return { ...e, owned: true, equipped: slotEmpty || e.equipped };
|
||||||
|
})
|
||||||
|
: prev.equipment ?? [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loss: reset boss HP and apply casualties
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
bosses: prev.bosses.map((b) =>
|
||||||
|
b.id === bossId ? { ...b, status: "available" as const, currentHp: b.maxHp } : b,
|
||||||
|
),
|
||||||
|
adventurers: prev.adventurers.map((a) => {
|
||||||
|
const casualty = result.casualties?.find((c) => c.adventurerId === a.id);
|
||||||
|
if (!casualty) return a;
|
||||||
|
return { ...a, count: Math.max(0, a.count - casualty.killed) };
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
import {
|
import {
|
||||||
createContext,
|
createContext,
|
||||||
useCallback,
|
useCallback,
|
||||||
@@ -85,6 +172,10 @@ interface GameContextValue {
|
|||||||
buyPrestigeUpgrade: (upgradeId: string) => Promise<void>;
|
buyPrestigeUpgrade: (upgradeId: string) => Promise<void>;
|
||||||
/** Toggle the auto-prestige setting on/off (requires auto_prestige upgrade) */
|
/** Toggle the auto-prestige setting on/off (requires auto_prestige upgrade) */
|
||||||
toggleAutoPrestige: () => void;
|
toggleAutoPrestige: () => void;
|
||||||
|
/** Toggle the auto-quest setting on/off */
|
||||||
|
toggleAutoQuest: () => void;
|
||||||
|
/** Toggle the auto-boss setting on/off */
|
||||||
|
toggleAutoBoss: () => void;
|
||||||
/** Queue of newly unlocked codex entry IDs (for toast notifications) */
|
/** Queue of newly unlocked codex entry IDs (for toast notifications) */
|
||||||
newCodexEntryIds: string[];
|
newCodexEntryIds: string[];
|
||||||
/** Remove a codex entry ID from the notification queue */
|
/** Remove a codex entry ID from the notification queue */
|
||||||
@@ -137,6 +228,7 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
|
|||||||
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 isAutoPrestigingRef = useRef(false);
|
||||||
|
const isAutoBossingRef = useRef(false);
|
||||||
const reloadRef = useRef<() => Promise<void>>(() => Promise.resolve());
|
const reloadRef = useRef<() => Promise<void>>(() => Promise.resolve());
|
||||||
const [newCodexEntryIds, setNewCodexEntryIds] = useState<string[]>([]);
|
const [newCodexEntryIds, setNewCodexEntryIds] = useState<string[]>([]);
|
||||||
const codexProcessedRef = useRef<Set<string>>(new Set());
|
const codexProcessedRef = useRef<Set<string>>(new Set());
|
||||||
@@ -314,7 +406,33 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
|
|||||||
|
|
||||||
setState((prev) => {
|
setState((prev) => {
|
||||||
if (!prev) return prev;
|
if (!prev) return prev;
|
||||||
const next = applyTick(prev, deltaSeconds);
|
let next = applyTick(prev, deltaSeconds);
|
||||||
|
|
||||||
|
// Auto-quest: start the highest-zone available quest when none is active
|
||||||
|
if (next.autoQuest) {
|
||||||
|
const hasActiveQuest = next.quests.some((q) => q.status === "active");
|
||||||
|
if (!hasActiveQuest) {
|
||||||
|
const partyCombatPower = next.adventurers.reduce(
|
||||||
|
(total, a) => total + a.combatPower * a.count,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
const zoneOrder = new Map(next.zones.map((z, i) => [z.id, i]));
|
||||||
|
const candidates = next.quests
|
||||||
|
.filter((q) => q.status === "available" && (q.combatPowerRequired ?? 0) <= partyCombatPower)
|
||||||
|
.sort((a, b) => (zoneOrder.get(b.zoneId) ?? 0) - (zoneOrder.get(a.zoneId) ?? 0));
|
||||||
|
const best = candidates[0];
|
||||||
|
if (best) {
|
||||||
|
next = {
|
||||||
|
...next,
|
||||||
|
quests: next.quests.map((q) =>
|
||||||
|
q.id === best.id
|
||||||
|
? { ...q, status: "active" as const, startedAt: Date.now() }
|
||||||
|
: q,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Detect newly unlocked achievements
|
// Detect newly unlocked achievements
|
||||||
newlyUnlockedRef.current = next.achievements.filter((a, i) => {
|
newlyUnlockedRef.current = next.achievements.filter((a, i) => {
|
||||||
@@ -370,6 +488,30 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
|
|||||||
.finally(() => { isAutoPrestigingRef.current = false; });
|
.finally(() => { isAutoPrestigingRef.current = false; });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto-boss: challenge the highest-zone available boss when not already fighting
|
||||||
|
if (!isAutoBossingRef.current && autoState?.autoBoss) {
|
||||||
|
const prestigeCount = autoState.prestige.count;
|
||||||
|
const zoneOrder = new Map((autoState.zones ?? []).map((z, i) => [z.id, i]));
|
||||||
|
const availableBoss = autoState.bosses
|
||||||
|
.filter((b) => b.status === "available" && b.prestigeRequirement <= prestigeCount)
|
||||||
|
.sort((a, b) => (zoneOrder.get(b.zoneId) ?? 0) - (zoneOrder.get(a.zoneId) ?? 0))[0];
|
||||||
|
if (availableBoss) {
|
||||||
|
const bossId = availableBoss.id;
|
||||||
|
const bossName = availableBoss.name;
|
||||||
|
isAutoBossingRef.current = true;
|
||||||
|
void challengeBossApi({ bossId })
|
||||||
|
.then((result) => {
|
||||||
|
setState((prev) => {
|
||||||
|
if (!prev) return prev;
|
||||||
|
return applyBossResult(prev, bossId, result);
|
||||||
|
});
|
||||||
|
setBattleResult({ bossName, result });
|
||||||
|
})
|
||||||
|
.catch(() => { /* silently ignore — will retry next tick */ })
|
||||||
|
.finally(() => { isAutoBossingRef.current = false; });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
rafRef.current = requestAnimationFrame(tick);
|
rafRef.current = requestAnimationFrame(tick);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -743,6 +885,20 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const toggleAutoQuest = useCallback(() => {
|
||||||
|
setState((prev) => {
|
||||||
|
if (!prev) return prev;
|
||||||
|
return { ...prev, autoQuest: !prev.autoQuest };
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const toggleAutoBoss = useCallback(() => {
|
||||||
|
setState((prev) => {
|
||||||
|
if (!prev) return prev;
|
||||||
|
return { ...prev, autoBoss: !prev.autoBoss };
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
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);
|
||||||
@@ -750,108 +906,10 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await challengeBossApi({ bossId });
|
const result = await challengeBossApi({ bossId });
|
||||||
|
|
||||||
// Update local state to match server result
|
|
||||||
setState((prev) => {
|
setState((prev) => {
|
||||||
if (!prev) return prev;
|
if (!prev) return prev;
|
||||||
|
return applyBossResult(prev, bossId, result);
|
||||||
if (result.won) {
|
|
||||||
const defeatedBoss = prev.bosses.find((b) => b.id === bossId);
|
|
||||||
const zoneBosses = prev.bosses.filter((b) => b.zoneId === defeatedBoss?.zoneId);
|
|
||||||
const zoneIdx = zoneBosses.findIndex((b) => b.id === bossId);
|
|
||||||
const nextZoneBossId = zoneBosses[zoneIdx + 1]?.id;
|
|
||||||
|
|
||||||
// Find newly unlocked zones and their first bosses
|
|
||||||
// A zone unlocks when BOTH the gate boss is defeated AND the gate quest is completed
|
|
||||||
const newlyUnlockedZones = (prev.zones ?? []).filter((z) => {
|
|
||||||
if (z.status !== "locked" || z.unlockBossId !== bossId) return false;
|
|
||||||
const questOk =
|
|
||||||
z.unlockQuestId == null ||
|
|
||||||
prev.quests.some((q) => q.id === z.unlockQuestId && q.status === "completed");
|
|
||||||
return questOk;
|
|
||||||
});
|
|
||||||
const newZoneFirstBossIds = newlyUnlockedZones.map((z) => {
|
|
||||||
const firstBoss = prev.bosses.find((b) => b.zoneId === z.id);
|
|
||||||
return firstBoss?.id;
|
|
||||||
}).filter(Boolean);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
bosses: prev.bosses.map((b) => {
|
|
||||||
if (b.id === bossId) return { ...b, status: "defeated" as const, currentHp: 0 };
|
|
||||||
if (b.id === nextZoneBossId && b.prestigeRequirement <= prev.prestige.count) {
|
|
||||||
return { ...b, status: "available" as const };
|
|
||||||
}
|
|
||||||
if (newZoneFirstBossIds.includes(b.id) && b.prestigeRequirement <= prev.prestige.count) {
|
|
||||||
return { ...b, status: "available" as const };
|
|
||||||
}
|
|
||||||
return b;
|
|
||||||
}),
|
|
||||||
zones: (prev.zones ?? []).map((z) => {
|
|
||||||
if (z.status !== "locked" || z.unlockBossId !== bossId) return z;
|
|
||||||
const questOk =
|
|
||||||
z.unlockQuestId == null ||
|
|
||||||
prev.quests.some((q) => q.id === z.unlockQuestId && q.status === "completed");
|
|
||||||
return questOk ? { ...z, status: "unlocked" as const } : z;
|
|
||||||
}),
|
|
||||||
resources: result.rewards
|
|
||||||
? {
|
|
||||||
...prev.resources,
|
|
||||||
gold: prev.resources.gold + result.rewards.gold,
|
|
||||||
essence: prev.resources.essence + result.rewards.essence,
|
|
||||||
crystals: prev.resources.crystals + result.rewards.crystals,
|
|
||||||
}
|
|
||||||
: prev.resources,
|
|
||||||
prestige: result.rewards?.bountyRunestones
|
|
||||||
? {
|
|
||||||
...prev.prestige,
|
|
||||||
runestones: prev.prestige.runestones + result.rewards.bountyRunestones,
|
|
||||||
}
|
|
||||||
: prev.prestige,
|
|
||||||
player: result.rewards
|
|
||||||
? {
|
|
||||||
...prev.player,
|
|
||||||
totalGoldEarned:
|
|
||||||
prev.player.totalGoldEarned + result.rewards.gold,
|
|
||||||
}
|
|
||||||
: prev.player,
|
|
||||||
upgrades: result.rewards
|
|
||||||
? prev.upgrades.map((u) =>
|
|
||||||
result.rewards!.upgradeIds.includes(u.id)
|
|
||||||
? { ...u, unlocked: true }
|
|
||||||
: u,
|
|
||||||
)
|
|
||||||
: prev.upgrades,
|
|
||||||
equipment: result.rewards
|
|
||||||
? (prev.equipment ?? []).map((e) => {
|
|
||||||
if (!result.rewards!.equipmentIds.includes(e.id)) return e;
|
|
||||||
const slotEmpty = !(prev.equipment ?? []).some(
|
|
||||||
(other) => other.type === e.type && other.equipped,
|
|
||||||
);
|
|
||||||
return { ...e, owned: true, equipped: slotEmpty || e.equipped };
|
|
||||||
})
|
|
||||||
: prev.equipment ?? [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loss: reset boss HP and apply casualties
|
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
bosses: prev.bosses.map((b) =>
|
|
||||||
b.id === bossId
|
|
||||||
? { ...b, status: "available" as const, currentHp: b.maxHp }
|
|
||||||
: b,
|
|
||||||
),
|
|
||||||
adventurers: prev.adventurers.map((a) => {
|
|
||||||
const casualty = result.casualties?.find(
|
|
||||||
(c) => c.adventurerId === a.id,
|
|
||||||
);
|
|
||||||
if (!casualty) return a;
|
|
||||||
return { ...a, count: Math.max(0, a.count - casualty.killed) };
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
setBattleResult({ bossName: boss.name, result });
|
setBattleResult({ bossName: boss.name, result });
|
||||||
} catch {
|
} catch {
|
||||||
// Silently ignore — server errors shouldn't crash the UI
|
// Silently ignore — server errors shouldn't crash the UI
|
||||||
@@ -914,6 +972,8 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
|
|||||||
formatNumber: boundFormatNumber,
|
formatNumber: boundFormatNumber,
|
||||||
buyPrestigeUpgrade,
|
buyPrestigeUpgrade,
|
||||||
toggleAutoPrestige,
|
toggleAutoPrestige,
|
||||||
|
toggleAutoQuest,
|
||||||
|
toggleAutoBoss,
|
||||||
newCodexEntryIds,
|
newCodexEntryIds,
|
||||||
dismissCodexEntry,
|
dismissCodexEntry,
|
||||||
transcend,
|
transcend,
|
||||||
|
|||||||
@@ -3766,6 +3766,43 @@ body {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Auto Toggle Buttons ────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.panel-header-controls {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-toggle-btn {
|
||||||
|
border-radius: var(--radius);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.3rem 0.7rem;
|
||||||
|
transition: background 0.15s, color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-toggle-off {
|
||||||
|
background: var(--colour-surface);
|
||||||
|
border: 1px solid var(--colour-border);
|
||||||
|
color: var(--colour-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-toggle-off:hover {
|
||||||
|
border-color: var(--colour-accent);
|
||||||
|
color: var(--colour-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-toggle-on {
|
||||||
|
background: var(--colour-accent);
|
||||||
|
border: 1px solid var(--colour-accent);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-toggle-on:hover {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Login Bonus Modal ──────────────────────────────────────────────── */
|
/* ── Login Bonus Modal ──────────────────────────────────────────────── */
|
||||||
|
|
||||||
.login-bonus-modal {
|
.login-bonus-modal {
|
||||||
|
|||||||
@@ -39,4 +39,8 @@ export interface GameState {
|
|||||||
apotheosis?: ApotheosisData;
|
apotheosis?: ApotheosisData;
|
||||||
/** Exploration and crafting state — optional for backwards compatibility */
|
/** Exploration and crafting state — optional for backwards compatibility */
|
||||||
exploration?: ExplorationState;
|
exploration?: ExplorationState;
|
||||||
|
/** When true, the tick engine automatically starts the highest-zone available quest */
|
||||||
|
autoQuest?: boolean;
|
||||||
|
/** When true, the tick engine automatically challenges the highest available boss */
|
||||||
|
autoBoss?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user