feat: another balance and bug fix pass (#238)
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 1m10s
CI / Lint, Build & Test (push) Successful in 1m13s

Working through open issues — fixes, balance changes, and features.

## Closed

- Closes #161
- Closes #181
- Closes #191
- Closes #199
- Closes #201
- Closes #202
- Closes #203
- Closes #204
- Closes #205
- Closes #206
- Closes #208
- Closes #211
- Closes #212
- Closes #213
- Closes #214
- Closes #216
- Closes #219
- Closes #220
- Closes #221
- Closes #222
- Closes #224
- Closes #225
- Closes #226
- Closes #228
- Closes #229
- Closes #230
- Closes #231
- Closes #232
- Closes #233
- Closes #234
- Closes #235
- Closes #236

 This PR was created with help from Hikari~ 🌸

Reviewed-on: #238
Co-authored-by: Hikari <hikari@nhcarrigan.com>
Co-committed-by: Hikari <hikari@nhcarrigan.com>
This commit was merged in pull request #238.
This commit is contained in:
2026-04-06 18:17:00 -07:00
committed by Naomi Carrigan
parent b0227c1709
commit 1195b657a0
34 changed files with 980 additions and 203 deletions
+109 -5
View File
@@ -22,6 +22,7 @@ import {
type NumberFormat,
type Quest,
type TranscendenceResponse,
computeUnlockedCompanionIds,
isStoryChapterUnlocked,
} from "@elysium/types";
import {
@@ -62,14 +63,16 @@ import {
computePartyCombatPower,
} from "../engine/tick.js";
import { updateChallengeProgress } from "../utils/dailyChallenges.js";
import { formatNumber as formatNumberUtil } from "../utils/format.js";
import {
formatInteger as formatIntegerUtil,
formatNumber as formatNumberUtil,
} from "../utils/format.js";
import { logError } from "../utils/logError.js";
import { sendNotification } from "../utils/notification.js";
import { playSound } from "../utils/sound.js";
const autoSaveIntervalMs = 30_000;
const autoPrestigeThresholdBase = 1_000_000;
const autoPrestigeThresholdScale = 5;
/**
* Pure function — applies a boss challenge result to the game state.
@@ -461,6 +464,11 @@ interface GameContextValue {
*/
formatNumber: (value: number)=> string;
/**
* Format a whole-number value without decimal places.
*/
formatInteger: (value: number)=> string;
/**
* Buy a prestige upgrade from the runestone shop.
*/
@@ -471,6 +479,11 @@ interface GameContextValue {
*/
toggleAutoPrestige: ()=> void;
/**
* Toggle whether auto-prestige waits for maximum runestone yield before firing.
*/
toggleAutoPrestigeMaxRunestones: ()=> void;
/**
* Toggle the auto-quest setting on/off.
*/
@@ -1111,6 +1124,57 @@ export const GameProvider = ({
});
}, [ state ]);
// Detect newly unlocked companions whenever relevant state changes
useEffect(() => {
if (state === null) {
return;
}
const computedUnlocks = computeUnlockedCompanionIds({
apotheosisCount: state.apotheosis?.count ?? 0,
lifetimeBossesDefeated: state.player.lifetimeBossesDefeated,
lifetimeGoldEarned: state.player.lifetimeGoldEarned,
lifetimeQuestsCompleted: state.player.lifetimeQuestsCompleted,
prestigeCount: state.prestige.count,
transcendenceCount: state.transcendence?.count ?? 0,
});
const currentUnlocks = state.companions?.unlockedCompanionIds ?? [];
const toAdd = computedUnlocks.filter((id) => {
return !currentUnlocks.includes(id);
});
if (toAdd.length === 0) {
return;
}
setState((previous) => {
if (previous === null) {
return previous;
}
const existingUnlocks = previous.companions?.unlockedCompanionIds ?? [];
const addedIds = computedUnlocks.filter((id) => {
return !existingUnlocks.includes(id);
});
if (addedIds.length === 0) {
return previous;
}
const updatedUnlocks = [ ...existingUnlocks, ...addedIds ];
const activeId = previous.companions?.activeCompanionId ?? null;
const validatedActiveId
= activeId !== null && updatedUnlocks.includes(activeId)
? activeId
: null;
return {
...previous,
companions: {
activeCompanionId: validatedActiveId,
unlockedCompanionIds: updatedUnlocks,
},
};
});
}, [ state ]);
// Game loop via requestAnimationFrame
useEffect(() => {
@@ -1332,14 +1396,27 @@ export const GameProvider = ({
// Auto-prestige: fire when unlocked, enabled, and threshold is met
const autoState = stateReference.current;
const autoPrestigeThreshold = autoPrestigeThresholdBase
* Math.pow((autoState?.prestige.count ?? 0) + 1, 2.5)
* (autoState?.transcendence?.echoPrestigeThresholdMultiplier ?? 1);
const autoBaseRunestones = Math.min(
Math.floor(
Math.cbrt(
(autoState?.player.totalGoldEarned ?? 0) / autoPrestigeThreshold,
),
) * 15,
200,
);
const autoMaxRunestonesMet
= autoState?.prestige.autoPrestigeMaxRunestonesOnly !== true
|| autoBaseRunestones >= 200;
if (
!isAutoPrestigingReference.current
&& autoState?.prestige.purchasedUpgradeIds.includes("auto_prestige")
=== true
&& autoState.prestige.autoPrestigeEnabled === true
&& autoState.player.totalGoldEarned
>= autoPrestigeThresholdBase
* Math.pow(autoPrestigeThresholdScale, autoState.prestige.count)
&& autoState.player.totalGoldEarned >= autoPrestigeThreshold
&& autoMaxRunestonesMet
) {
isAutoPrestigingReference.current = true;
void prestigeApi({}).
@@ -2005,6 +2082,22 @@ export const GameProvider = ({
});
}, []);
const toggleAutoPrestigeMaxRunestones = useCallback(() => {
setState((previous) => {
if (previous === null) {
return previous;
}
return {
...previous,
prestige: {
...previous.prestige,
autoPrestigeMaxRunestonesOnly:
previous.prestige.autoPrestigeMaxRunestonesOnly !== true,
},
};
});
}, []);
const toggleAutoQuest = useCallback(() => {
setState((previous) => {
if (previous === null) {
@@ -2359,6 +2452,13 @@ export const GameProvider = ({
[ numberFormat ],
);
const formatInteger = useCallback(
(value: number) => {
return formatIntegerUtil(value);
},
[],
);
const contextValue = useMemo<GameContextValue>(() => {
return {
apotheosis,
@@ -2397,6 +2497,7 @@ export const GameProvider = ({
flushBossLoreToasts,
forceSync,
forceUnlocks,
formatInteger,
formatNumber,
handleClick,
inGuild,
@@ -2428,6 +2529,7 @@ export const GameProvider = ({
toggleAutoAdventurer,
toggleAutoBoss,
toggleAutoPrestige,
toggleAutoPrestigeMaxRunestones,
toggleAutoQuest,
transcend,
triggerPrestigeToast,
@@ -2443,6 +2545,7 @@ export const GameProvider = ({
bossError,
completedQuestToasts,
failedQuestToasts,
formatInteger,
formatNumber,
buyAdventurer,
buyEchoUpgrade,
@@ -2502,6 +2605,7 @@ export const GameProvider = ({
toggleAutoAdventurer,
toggleAutoBoss,
toggleAutoPrestige,
toggleAutoPrestigeMaxRunestones,
toggleAutoQuest,
transcend,
triggerPrestigeToast,