generated from nhcarrigan/template
fix: apply crystal multiplier to boss rewards, steepen prestige threshold, fix stale upgrade race condition, and fix companion format display
Closes #221 Closes #222 Closes #201 Closes #213
This commit is contained in:
@@ -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,12 @@ 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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -72,6 +44,7 @@ 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.
|
||||
* @returns The JSX element.
|
||||
*/
|
||||
const CompanionCard = ({
|
||||
@@ -79,6 +52,7 @@ const CompanionCard = ({
|
||||
isUnlocked,
|
||||
isActive,
|
||||
onSelect,
|
||||
formatNumber,
|
||||
}: CompanionCardProperties): JSX.Element => {
|
||||
const bonusSign = companion.bonus.type === "questTime"
|
||||
? "-"
|
||||
@@ -138,10 +112,9 @@ const CompanionCard = ({
|
||||
</button>
|
||||
: <div className="companion-unlock-requirement">
|
||||
{"🔒 Unlock: "}
|
||||
{formatThreshold(
|
||||
companion.unlock.type,
|
||||
companion.unlock.threshold,
|
||||
)}{" "}
|
||||
{companion.unlock.type === "lifetimeGold"
|
||||
? formatNumber(companion.unlock.threshold)
|
||||
: String(companion.unlock.threshold)}{" "}
|
||||
{unlockLabels[companion.unlock.type] ?? companion.unlock.type}
|
||||
</div>
|
||||
}
|
||||
@@ -154,7 +127,7 @@ const CompanionCard = ({
|
||||
* @returns The JSX element.
|
||||
*/
|
||||
const CompanionPanel = (): JSX.Element => {
|
||||
const { state, setActiveCompanion } = useGame();
|
||||
const { formatNumber, setActiveCompanion, state } = useGame();
|
||||
|
||||
if (state === null) {
|
||||
return (
|
||||
@@ -204,6 +177,7 @@ const CompanionPanel = (): JSX.Element => {
|
||||
return (
|
||||
<CompanionCard
|
||||
companion={companion}
|
||||
formatNumber={formatNumber}
|
||||
isActive={activeId === companion.id}
|
||||
isUnlocked={unlockedIds.includes(companion.id)}
|
||||
key={companion.id}
|
||||
|
||||
@@ -27,12 +27,12 @@ const baseThreshold = 1_000_000;
|
||||
|
||||
/**
|
||||
* Calculates the prestige threshold for a given prestige count.
|
||||
* Mirrors the server formula: BASE * (count + 1)^2.
|
||||
* Mirrors the server formula: BASE * (count + 1)^2.5.
|
||||
* @param prestigeCount - The current prestige count.
|
||||
* @returns The required gold to prestige.
|
||||
*/
|
||||
const calculateThreshold = (prestigeCount: number): number => {
|
||||
return baseThreshold * Math.pow(prestigeCount + 1, 2);
|
||||
return baseThreshold * Math.pow(prestigeCount + 1, 2.5);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -465,7 +465,7 @@ export const computeProjectedRunestones = (state: GameState): number => {
|
||||
const thresholdMult: number
|
||||
= state.transcendence?.echoPrestigeThresholdMultiplier ?? 1;
|
||||
const threshold
|
||||
= basePrestigeThreshold * Math.pow(count + 1, 2) * thresholdMult;
|
||||
= basePrestigeThreshold * Math.pow(count + 1, 2.5) * thresholdMult;
|
||||
const base = Math.min(
|
||||
Math.floor(Math.cbrt(state.player.totalGoldEarned / threshold))
|
||||
* runestonesPerPrestigeLevelClient,
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { formatNumber } from "../src/utils/format.js";
|
||||
import { formatInteger, formatNumber } from "../src/utils/format.js";
|
||||
|
||||
describe("formatNumber", () => {
|
||||
describe("edge cases", () => {
|
||||
@@ -142,3 +142,59 @@ describe("formatNumber", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatInteger", () => {
|
||||
describe("edge cases", () => {
|
||||
it("should return '0' for NaN", () => {
|
||||
expect(formatInteger(Number.NaN)).toBe("0");
|
||||
});
|
||||
|
||||
it("should return '0' for Infinity", () => {
|
||||
expect(formatInteger(Infinity)).toBe("0");
|
||||
});
|
||||
|
||||
it("should format negative integers with a leading minus sign", () => {
|
||||
expect(formatInteger(-1500)).toBe("-1K");
|
||||
});
|
||||
|
||||
it("should format zero as '0'", () => {
|
||||
expect(formatInteger(0)).toBe("0");
|
||||
});
|
||||
|
||||
it("should format small integers without decimals", () => {
|
||||
expect(formatInteger(42)).toBe("42");
|
||||
});
|
||||
});
|
||||
|
||||
describe("named suffixes", () => {
|
||||
it("should format thousands with K suffix and no decimals", () => {
|
||||
expect(formatInteger(1500)).toBe("1K");
|
||||
});
|
||||
|
||||
it("should format millions with M suffix and no decimals", () => {
|
||||
expect(formatInteger(2_500_000)).toBe("2M");
|
||||
});
|
||||
|
||||
it("should format billions with B suffix", () => {
|
||||
expect(formatInteger(3_000_000_000)).toBe("3B");
|
||||
});
|
||||
|
||||
it("should format trillions with T suffix", () => {
|
||||
expect(formatInteger(1e12)).toBe("1T");
|
||||
});
|
||||
|
||||
it("should format quintillions with Qi suffix", () => {
|
||||
expect(formatInteger(1e18)).toBe("1Qi");
|
||||
});
|
||||
});
|
||||
|
||||
describe("letter suffixes", () => {
|
||||
it("should format values >= 1e36 with letter suffix 'a'", () => {
|
||||
expect(formatInteger(1e36)).toBe("1a");
|
||||
});
|
||||
|
||||
it("should format values >= 1e39 with letter suffix 'b'", () => {
|
||||
expect(formatInteger(1e39)).toBe("1b");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user