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),
|
achievements: structuredClone(defaultAchievements),
|
||||||
adventurers: structuredClone(defaultAdventurers),
|
adventurers: structuredClone(defaultAdventurers),
|
||||||
apotheosis: { ...initialApotheosis },
|
apotheosis: { ...initialApotheosis },
|
||||||
|
autoBoss: false,
|
||||||
|
autoQuest: false,
|
||||||
baseClickPower: 1,
|
baseClickPower: 1,
|
||||||
bosses: structuredClone(defaultBosses),
|
bosses: structuredClone(defaultBosses),
|
||||||
companions: { activeCompanionId: null, unlockedCompanionIds: [] },
|
companions: { activeCompanionId: null, unlockedCompanionIds: [] },
|
||||||
|
|||||||
@@ -226,7 +226,14 @@ const computePartyStats = (
|
|||||||
* @returns The JSX element.
|
* @returns The JSX element.
|
||||||
*/
|
*/
|
||||||
const BossPanel = (): 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>(
|
const [ challengingBossId, setChallengingBossId ] = useState<string | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
@@ -346,6 +353,23 @@ const BossPanel = (): JSX.Element => {
|
|||||||
</div>
|
</div>
|
||||||
</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
|
<ZoneSelector
|
||||||
activeZoneId={activeZoneId}
|
activeZoneId={activeZoneId}
|
||||||
onSelectZone={setActiveZoneId}
|
onSelectZone={setActiveZoneId}
|
||||||
|
|||||||
@@ -545,6 +545,18 @@ interface GameContextValue {
|
|||||||
* Reset all progress to a fresh save state (resolves schema outdated).
|
* Reset all progress to a fresh save state (resolves schema outdated).
|
||||||
*/
|
*/
|
||||||
resetProgress: ()=> Promise<void>;
|
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 {
|
export interface BattleResult {
|
||||||
@@ -588,6 +600,12 @@ export const GameProvider = ({
|
|||||||
const [ lastSavedAt, setLastSavedAt ] = useState<number | null>(null);
|
const [ lastSavedAt, setLastSavedAt ] = useState<number | null>(null);
|
||||||
const [ isSyncing, setIsSyncing ] = useState(false);
|
const [ isSyncing, setIsSyncing ] = useState(false);
|
||||||
const [ syncError, setSyncError ] = useState<string | null>(null);
|
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>(
|
const syncErrorTimerReference = useRef<ReturnType<typeof setTimeout> | null>(
|
||||||
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;
|
return next;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1200,14 +1226,32 @@ export const GameProvider = ({
|
|||||||
if (previous === null) {
|
if (previous === null) {
|
||||||
return previous;
|
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) => {
|
catch((error_: unknown) => {
|
||||||
logError("auto_boss", error_);
|
logError("auto_boss", error_);
|
||||||
|
const message
|
||||||
/* Silently ignore — will retry next tick */
|
= error_ instanceof Error
|
||||||
|
? error_.message
|
||||||
|
: String(error_);
|
||||||
|
setAutoBossError(message);
|
||||||
|
setState((previous) => {
|
||||||
|
if (previous === null) {
|
||||||
|
return previous;
|
||||||
|
}
|
||||||
|
return { ...previous, autoBoss: false };
|
||||||
|
});
|
||||||
}).
|
}).
|
||||||
finally(() => {
|
finally(() => {
|
||||||
isAutoBossingReference.current = false;
|
isAutoBossingReference.current = false;
|
||||||
@@ -1782,6 +1826,8 @@ export const GameProvider = ({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const toggleAutoBoss = useCallback(() => {
|
const toggleAutoBoss = useCallback(() => {
|
||||||
|
setAutoBossError(null);
|
||||||
|
setAutoBossLastResult(null);
|
||||||
setState((previous) => {
|
setState((previous) => {
|
||||||
if (previous === null) {
|
if (previous === null) {
|
||||||
return previous;
|
return previous;
|
||||||
@@ -1974,6 +2020,8 @@ export const GameProvider = ({
|
|||||||
const contextValue = useMemo<GameContextValue>(() => {
|
const contextValue = useMemo<GameContextValue>(() => {
|
||||||
return {
|
return {
|
||||||
apotheosis,
|
apotheosis,
|
||||||
|
autoBossError,
|
||||||
|
autoBossLastResult,
|
||||||
battleResult,
|
battleResult,
|
||||||
buyAdventurer,
|
buyAdventurer,
|
||||||
buyEchoUpgrade,
|
buyEchoUpgrade,
|
||||||
@@ -2040,6 +2088,8 @@ export const GameProvider = ({
|
|||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
apotheosis,
|
apotheosis,
|
||||||
|
autoBossError,
|
||||||
|
autoBossLastResult,
|
||||||
battleResult,
|
battleResult,
|
||||||
completedQuestToasts,
|
completedQuestToasts,
|
||||||
failedQuestToasts,
|
failedQuestToasts,
|
||||||
|
|||||||
Reference in New Issue
Block a user