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
+47 -41
View File
@@ -6,6 +6,7 @@
*/
/* eslint-disable react/no-multi-comp -- Sub-component is tightly coupled to the panel */
/* eslint-disable max-lines-per-function -- Complex companion card with conditional renders */
/* eslint-disable complexity -- Companion card has many conditional render paths */
import { COMPANIONS, type Companion } from "@elysium/types";
import { useGame } from "../../context/gameContext.js";
import { cdnImage } from "../../utils/cdn.js";
@@ -28,41 +29,13 @@ const unlockLabels: Record<string, string> = {
transcendence: "transcendence(s)",
};
/**
* Formats a companion unlock threshold for display.
* @param type - The unlock condition type.
* @param threshold - The threshold value.
* @returns The formatted threshold string.
*/
const formatThreshold = (type: string, threshold: number): string => {
if (type === "lifetimeGold") {
if (threshold >= 1e18) {
return `${(threshold / 1e18).toFixed(0)}Qt`;
}
if (threshold >= 1e15) {
return `${(threshold / 1e15).toFixed(0)}Q`;
}
if (threshold >= 1e12) {
return `${(threshold / 1e12).toFixed(0)}T`;
}
if (threshold >= 1e9) {
return `${(threshold / 1e9).toFixed(0)}B`;
}
if (threshold >= 1e6) {
return `${(threshold / 1e6).toFixed(0)}M`;
}
if (threshold >= 1e3) {
return `${(threshold / 1e3).toFixed(0)}K`;
}
}
return threshold.toString();
};
interface CompanionCardProperties {
readonly companion: Companion;
readonly isUnlocked: boolean;
readonly isActive: boolean;
readonly onSelect: ()=> void;
readonly companion: Companion;
readonly isUnlocked: boolean;
readonly isActive: boolean;
readonly onSelect: ()=> void;
readonly formatNumber: (n: number)=> string;
readonly currentProgress: number;
}
/**
@@ -72,6 +45,8 @@ interface CompanionCardProperties {
* @param props.isUnlocked - Whether this companion is unlocked.
* @param props.isActive - Whether this companion is currently active.
* @param props.onSelect - Callback when the companion is selected/deselected.
* @param props.formatNumber - The number formatting utility function.
* @param props.currentProgress - The player's current progress toward the unlock threshold.
* @returns The JSX element.
*/
const CompanionCard = ({
@@ -79,6 +54,8 @@ const CompanionCard = ({
isUnlocked,
isActive,
onSelect,
formatNumber,
currentProgress,
}: CompanionCardProperties): JSX.Element => {
const bonusSign = companion.bonus.type === "questTime"
? "-"
@@ -137,12 +114,28 @@ const CompanionCard = ({
: "Activate"}
</button>
: <div className="companion-unlock-requirement">
{"🔒 Unlock: "}
{formatThreshold(
companion.unlock.type,
companion.unlock.threshold,
)}{" "}
{unlockLabels[companion.unlock.type] ?? companion.unlock.type}
<p>
{"🔒 Unlock: "}
{companion.unlock.type === "lifetimeGold"
? formatNumber(companion.unlock.threshold)
: String(companion.unlock.threshold)}{" "}
{unlockLabels[companion.unlock.type] ?? companion.unlock.type}
</p>
<div className="companion-progress">
<progress
max={companion.unlock.threshold}
value={Math.min(currentProgress, companion.unlock.threshold)}
/>
<span className="companion-progress-label">
{companion.unlock.type === "lifetimeGold"
? formatNumber(currentProgress)
: String(currentProgress)}
{" / "}
{companion.unlock.type === "lifetimeGold"
? formatNumber(companion.unlock.threshold)
: String(companion.unlock.threshold)}
</span>
</div>
</div>
}
</div>
@@ -154,7 +147,7 @@ const CompanionCard = ({
* @returns The JSX element.
*/
const CompanionPanel = (): JSX.Element => {
const { state, setActiveCompanion } = useGame();
const { formatNumber, setActiveCompanion, state } = useGame();
if (state === null) {
return (
@@ -167,6 +160,15 @@ const CompanionPanel = (): JSX.Element => {
const unlockedIds = state.companions?.unlockedCompanionIds ?? [];
const activeId = state.companions?.activeCompanionId ?? null;
const progressByUnlockType: Record<string, number> = {
apotheosis: state.apotheosis?.count ?? 0,
lifetimeBosses: state.player.lifetimeBossesDefeated,
lifetimeGold: state.player.lifetimeGoldEarned,
lifetimeQuests: state.player.lifetimeQuestsCompleted,
prestige: state.prestige.count,
transcendence: state.transcendence?.count ?? 0,
};
function handleSelect(companionId: string): void {
setActiveCompanion(activeId === companionId
? null
@@ -204,6 +206,10 @@ const CompanionPanel = (): JSX.Element => {
return (
<CompanionCard
companion={companion}
currentProgress={
progressByUnlockType[companion.unlock.type] ?? 0
}
formatNumber={formatNumber}
isActive={activeId === companion.id}
isUnlocked={unlockedIds.includes(companion.id)}
key={companion.id}