generated from nhcarrigan/template
fix: turn off auto-boss/auto-quest on failure and surface status (#46)
## Summary - Auto-boss now turns itself **off** when a boss fight is **lost**, so the player can reassess rather than the system silently looping. A "🤖 Last fight: [Boss] — ❌ Lost" status line appears in the boss panel. - Auto-boss also turns off (with an ⚠️ error message) when the API call fails outright (e.g. party has no adventurers), replacing the previous behaviour of silently hammering the API every animation frame. - Auto-quest now turns itself **off** whenever a quest fails the random-chance check, detected inside the tick's `setState` callback immediately after `applyTick`. - `autoBoss: false` and `autoQuest: false` are now part of `initialGameState`, so these fields persist through save/load cycles from the very first session — preventing a race window where the boss-route DB write could strip them before the first auto-save. - `toggleAutoBoss` clears both `autoBossLastResult` and `autoBossError` on each toggle so the panel always reflects the current session cleanly. ## Test plan - [x] `pnpm lint` — 0 errors, 0 warnings - [x] `pnpm build` — all packages clean - [x] `pnpm test` — 100% coverage maintained across the board Closes #40 ✨ This issue was created with help from Hikari~ 🌸 Reviewed-on: #46 Co-authored-by: Hikari <hikari@nhcarrigan.com> Co-committed-by: Hikari <hikari@nhcarrigan.com>
This commit was merged in pull request #46.
This commit is contained in:
@@ -76,6 +76,8 @@ const initialGameState = (
|
||||
achievements: structuredClone(defaultAchievements),
|
||||
adventurers: structuredClone(defaultAdventurers),
|
||||
apotheosis: { ...initialApotheosis },
|
||||
autoBoss: false,
|
||||
autoQuest: false,
|
||||
baseClickPower: 1,
|
||||
bosses: structuredClone(defaultBosses),
|
||||
companions: { activeCompanionId: null, unlockedCompanionIds: [] },
|
||||
|
||||
@@ -226,7 +226,14 @@ const computePartyStats = (
|
||||
* @returns The JSX element.
|
||||
*/
|
||||
const BossPanel = (): JSX.Element => {
|
||||
const { state, challengeBoss, formatNumber, toggleAutoBoss } = useGame();
|
||||
const {
|
||||
state,
|
||||
challengeBoss,
|
||||
formatNumber,
|
||||
toggleAutoBoss,
|
||||
autoBossLastResult,
|
||||
autoBossError,
|
||||
} = useGame();
|
||||
const [ challengingBossId, setChallengingBossId ] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
@@ -346,6 +353,23 @@ const BossPanel = (): JSX.Element => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{autoBossError === null
|
||||
? null
|
||||
: <p className="auto-boss-error">
|
||||
{"⚠️ Auto-boss stopped: "}
|
||||
{autoBossError}
|
||||
</p>
|
||||
}
|
||||
{autoBossLastResult !== null && autoBossError === null
|
||||
? <p className="auto-boss-status">
|
||||
{"🤖 Last fight: "}
|
||||
{autoBossLastResult.bossName}
|
||||
{autoBossLastResult.won
|
||||
? " — ✅ Won"
|
||||
: " — ❌ Lost"}
|
||||
</p>
|
||||
: null}
|
||||
|
||||
<ZoneSelector
|
||||
activeZoneId={activeZoneId}
|
||||
onSelectZone={setActiveZoneId}
|
||||
|
||||
@@ -545,6 +545,18 @@ interface GameContextValue {
|
||||
* Reset all progress to a fresh save state (resolves schema outdated).
|
||||
*/
|
||||
resetProgress: ()=> Promise<void>;
|
||||
|
||||
/**
|
||||
* Last auto-boss fight result — null until the first auto fight completes or
|
||||
* when auto-boss is toggled off.
|
||||
*/
|
||||
autoBossLastResult: { bossName: string; won: boolean; at: number } | null;
|
||||
|
||||
/**
|
||||
* Error message set when auto-boss stopped due to a critical failure (null
|
||||
* when no error). Cleared automatically when the player re-enables auto-boss.
|
||||
*/
|
||||
autoBossError: string | null;
|
||||
}
|
||||
|
||||
export interface BattleResult {
|
||||
@@ -588,6 +600,12 @@ export const GameProvider = ({
|
||||
const [ lastSavedAt, setLastSavedAt ] = useState<number | null>(null);
|
||||
const [ isSyncing, setIsSyncing ] = useState(false);
|
||||
const [ syncError, setSyncError ] = useState<string | null>(null);
|
||||
const [ autoBossLastResult, setAutoBossLastResult ] = useState<{
|
||||
bossName: string;
|
||||
won: boolean;
|
||||
at: number;
|
||||
} | null>(null);
|
||||
const [ autoBossError, setAutoBossError ] = useState<string | null>(null);
|
||||
const syncErrorTimerReference = useRef<ReturnType<typeof setTimeout> | null>(
|
||||
null,
|
||||
);
|
||||
@@ -1059,6 +1077,14 @@ export const GameProvider = ({
|
||||
},
|
||||
);
|
||||
|
||||
// Quest failure — turn off auto-quest so the player can reassess
|
||||
if (
|
||||
newlyFailedQuestsReference.current.length > 0
|
||||
&& next.autoQuest === true
|
||||
) {
|
||||
next = { ...next, autoQuest: false };
|
||||
}
|
||||
|
||||
return next;
|
||||
});
|
||||
|
||||
@@ -1200,14 +1226,32 @@ export const GameProvider = ({
|
||||
if (previous === null) {
|
||||
return previous;
|
||||
}
|
||||
return applyBossResult(previous, bossId, result);
|
||||
const afterBoss = applyBossResult(previous, bossId, result);
|
||||
// Defeat — turn off auto-boss so the player can reassess
|
||||
if (!result.won) {
|
||||
return { ...afterBoss, autoBoss: false };
|
||||
}
|
||||
return afterBoss;
|
||||
});
|
||||
setAutoBossLastResult({
|
||||
at: Date.now(),
|
||||
bossName: bossName,
|
||||
won: result.won,
|
||||
});
|
||||
setBattleResult({ bossName, result });
|
||||
}).
|
||||
catch((error_: unknown) => {
|
||||
logError("auto_boss", error_);
|
||||
|
||||
/* Silently ignore — will retry next tick */
|
||||
const message
|
||||
= error_ instanceof Error
|
||||
? error_.message
|
||||
: String(error_);
|
||||
setAutoBossError(message);
|
||||
setState((previous) => {
|
||||
if (previous === null) {
|
||||
return previous;
|
||||
}
|
||||
return { ...previous, autoBoss: false };
|
||||
});
|
||||
}).
|
||||
finally(() => {
|
||||
isAutoBossingReference.current = false;
|
||||
@@ -1782,6 +1826,8 @@ export const GameProvider = ({
|
||||
}, []);
|
||||
|
||||
const toggleAutoBoss = useCallback(() => {
|
||||
setAutoBossError(null);
|
||||
setAutoBossLastResult(null);
|
||||
setState((previous) => {
|
||||
if (previous === null) {
|
||||
return previous;
|
||||
@@ -1974,6 +2020,8 @@ export const GameProvider = ({
|
||||
const contextValue = useMemo<GameContextValue>(() => {
|
||||
return {
|
||||
apotheosis,
|
||||
autoBossError,
|
||||
autoBossLastResult,
|
||||
battleResult,
|
||||
buyAdventurer,
|
||||
buyEchoUpgrade,
|
||||
@@ -2040,6 +2088,8 @@ export const GameProvider = ({
|
||||
};
|
||||
}, [
|
||||
apotheosis,
|
||||
autoBossError,
|
||||
autoBossLastResult,
|
||||
battleResult,
|
||||
completedQuestToasts,
|
||||
failedQuestToasts,
|
||||
|
||||
Reference in New Issue
Block a user