generated from nhcarrigan/template
feat: respect integer format pref
This commit is contained in:
@@ -64,6 +64,7 @@ const BattleModal = ({
|
||||
flushBossLoreToasts,
|
||||
formatInteger,
|
||||
formatNumber,
|
||||
numberFormat,
|
||||
} = useGame();
|
||||
|
||||
const [ phase, setPhase ] = useState<"animating" | "result">("animating");
|
||||
@@ -242,14 +243,14 @@ const BattleModal = ({
|
||||
{result.rewards.crystals > 0
|
||||
&& <span>
|
||||
{"💎 "}
|
||||
{formatInteger(result.rewards.crystals)}
|
||||
{formatInteger(result.rewards.crystals, numberFormat)}
|
||||
{" crystals"}
|
||||
</span>
|
||||
}
|
||||
{result.rewards.bountyRunestones > 0
|
||||
&& <span className="battle-bounty">
|
||||
{"🔮 "}
|
||||
{formatInteger(result.rewards.bountyRunestones)}
|
||||
{formatInteger(result.rewards.bountyRunestones, numberFormat)}
|
||||
{" runestones (first kill!)"}
|
||||
</span>
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import { computePartyCombatPower } from "../../engine/tick.js";
|
||||
import { cdnImage } from "../../utils/cdn.js";
|
||||
import { LockToggle } from "../ui/lockToggle.js";
|
||||
import { ZoneSelector } from "./zoneSelector.js";
|
||||
import type { Boss } from "@elysium/types";
|
||||
import type { Boss, NumberFormat } from "@elysium/types";
|
||||
|
||||
interface BossCardProperties {
|
||||
readonly boss: Boss;
|
||||
@@ -23,7 +23,7 @@ interface BossCardProperties {
|
||||
readonly onChallenge: (bossId: string)=> void;
|
||||
readonly isChallenging: boolean;
|
||||
readonly unlockHint: string | undefined;
|
||||
readonly formatInteger: (n: number)=> string;
|
||||
readonly formatInteger: (n: number, format: NumberFormat)=> string;
|
||||
readonly formatNumber: (n: number)=> string;
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ const BossCard = ({
|
||||
{boss.crystalReward > 0
|
||||
&& <span>
|
||||
{"💎 "}
|
||||
{formatInteger(boss.crystalReward)}
|
||||
{formatInteger(boss.crystalReward, numberFormat)}
|
||||
</span>
|
||||
}
|
||||
{boss.equipmentRewards.length > 0
|
||||
@@ -175,6 +175,7 @@ const BossPanel = (): JSX.Element => {
|
||||
autoBossLastResult,
|
||||
autoBossError,
|
||||
bossError,
|
||||
numberFormat,
|
||||
} = useGame();
|
||||
const [ challengingBossId, setChallengingBossId ] = useState<string | null>(
|
||||
null,
|
||||
@@ -403,6 +404,7 @@ const BossPanel = (): JSX.Element => {
|
||||
formatNumber={formatNumber}
|
||||
isChallenging={challengingBossId === bossId}
|
||||
key={bossId}
|
||||
numberFormat={numberFormat}
|
||||
onChallenge={handleChallengeClick}
|
||||
prestigeCount={prestigeCount}
|
||||
unlockHint={bossUnlockHints.get(bossId)}
|
||||
|
||||
@@ -70,6 +70,7 @@ const PrestigePanel = (): JSX.Element => {
|
||||
toggleAutoPrestige,
|
||||
toggleAutoPrestigeMaxRunestones,
|
||||
triggerPrestigeToast,
|
||||
numberFormat,
|
||||
} = useGame();
|
||||
const [ isPending, setIsPending ] = useState(false);
|
||||
const [ result, setResult ] = useState<{
|
||||
@@ -198,7 +199,7 @@ const PrestigePanel = (): JSX.Element => {
|
||||
type="button"
|
||||
>
|
||||
{"🔮 Runestone Shop ("}
|
||||
{formatInteger(prestigeData.runestones)}
|
||||
{formatInteger(prestigeData.runestones, numberFormat)}
|
||||
{" stones)"}
|
||||
</button>
|
||||
</div>
|
||||
@@ -242,14 +243,14 @@ const PrestigePanel = (): JSX.Element => {
|
||||
</p>
|
||||
<p>
|
||||
{"Runestones: "}
|
||||
<strong>{formatInteger(prestigeData.runestones)}</strong>
|
||||
<strong>{formatInteger(prestigeData.runestones, numberFormat)}</strong>
|
||||
</p>
|
||||
{isEligible
|
||||
? <p className="runestone-preview">
|
||||
{"Runestones on prestige: "}
|
||||
<strong>
|
||||
{"+"}
|
||||
{formatInteger(runestonePreview)}
|
||||
{formatInteger(runestonePreview, numberFormat)}
|
||||
</strong>
|
||||
{isAtMaxRunestones
|
||||
? <span className="runestone-max-badge">{" ⚡ MAX"}</span>
|
||||
@@ -289,7 +290,7 @@ const PrestigePanel = (): JSX.Element => {
|
||||
>
|
||||
{isPending
|
||||
? "Ascending..."
|
||||
: `✨ Ascend (+${formatInteger(runestonePreview)} Runestones)`}
|
||||
: `✨ Ascend (+${formatInteger(runestonePreview, numberFormat)} Runestones)`}
|
||||
</button>
|
||||
{prestigeError === null
|
||||
? null
|
||||
@@ -301,12 +302,12 @@ const PrestigePanel = (): JSX.Element => {
|
||||
{"Ascended to Prestige "}
|
||||
{result.count}
|
||||
{"! Earned "}
|
||||
{formatInteger(result.runestones)}
|
||||
{formatInteger(result.runestones, numberFormat)}
|
||||
{" Runestones."}
|
||||
{result.milestoneRunestones > 0
|
||||
&& <>
|
||||
{" 🎉 Milestone bonus: +"}
|
||||
{formatInteger(result.milestoneRunestones)}
|
||||
{formatInteger(result.milestoneRunestones, numberFormat)}
|
||||
{" Runestones!"}
|
||||
</>
|
||||
}
|
||||
@@ -327,7 +328,7 @@ const PrestigePanel = (): JSX.Element => {
|
||||
<p className="shop-balance">
|
||||
{"Balance: "}
|
||||
<strong>
|
||||
{formatInteger(prestigeData.runestones)}
|
||||
{formatInteger(prestigeData.runestones, numberFormat)}
|
||||
{" Runestones"}
|
||||
</strong>
|
||||
</p>
|
||||
@@ -381,7 +382,7 @@ const PrestigePanel = (): JSX.Element => {
|
||||
<p className="upgrade-cost">
|
||||
{purchased
|
||||
? "✅ Purchased"
|
||||
: `🔮 ${formatInteger(upgrade.runestonesCost)} Runestones`}
|
||||
: `🔮 ${formatInteger(upgrade.runestonesCost, numberFormat)} Runestones`}
|
||||
</p>
|
||||
</div>
|
||||
{isAutoAdventurerToggle
|
||||
|
||||
@@ -59,7 +59,7 @@ const StatCard = ({
|
||||
* @returns The JSX element.
|
||||
*/
|
||||
const StatisticsPanel = (): JSX.Element => {
|
||||
const { state, formatInteger, formatNumber } = useGame();
|
||||
const { state, formatInteger, formatNumber, numberFormat } = useGame();
|
||||
|
||||
if (state === null) {
|
||||
return (
|
||||
@@ -152,13 +152,13 @@ const StatisticsPanel = (): JSX.Element => {
|
||||
<StatCard
|
||||
icon="💎"
|
||||
label="Crystals"
|
||||
value={formatInteger(resources.crystals)}
|
||||
value={formatInteger(resources.crystals, numberFormat)}
|
||||
/>
|
||||
<StatCard
|
||||
icon="🔮"
|
||||
label="Runestones"
|
||||
sub="permanent currency"
|
||||
value={formatInteger(prestige.runestones)}
|
||||
value={formatInteger(prestige.runestones, numberFormat)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ const categoryOrder: Array<TranscendenceUpgradeCategory> = [
|
||||
* @returns The JSX element.
|
||||
*/
|
||||
const TranscendencePanel = (): JSX.Element => {
|
||||
const { state, formatInteger, transcend, buyEchoUpgrade } = useGame();
|
||||
const { state, formatInteger, transcend, buyEchoUpgrade, numberFormat } = useGame();
|
||||
const [ isPending, setIsPending ] = useState(false);
|
||||
const [ result, setResult ] = useState<{
|
||||
echoes: number;
|
||||
@@ -152,7 +152,7 @@ const TranscendencePanel = (): JSX.Element => {
|
||||
type="button"
|
||||
>
|
||||
{"✨ Echo Shop ("}
|
||||
{formatInteger(currentEchoes)}
|
||||
{formatInteger(currentEchoes, numberFormat)}
|
||||
{" echoes)"}
|
||||
</button>
|
||||
</div>
|
||||
@@ -184,7 +184,7 @@ const TranscendencePanel = (): JSX.Element => {
|
||||
}
|
||||
<p>
|
||||
{"Current Echoes: "}
|
||||
<strong>{formatInteger(currentEchoes)}</strong>
|
||||
<strong>{formatInteger(currentEchoes, numberFormat)}</strong>
|
||||
</p>
|
||||
<p>
|
||||
{"Current prestige count: "}
|
||||
@@ -195,7 +195,7 @@ const TranscendencePanel = (): JSX.Element => {
|
||||
{"Echoes on transcendence: "}
|
||||
<strong>
|
||||
{"+"}
|
||||
{formatInteger(echoPreview)}
|
||||
{formatInteger(echoPreview, numberFormat)}
|
||||
</strong>
|
||||
{echoMetaMultiplier > 1
|
||||
&& <span className="echo-meta-bonus">
|
||||
@@ -238,7 +238,7 @@ const TranscendencePanel = (): JSX.Element => {
|
||||
>
|
||||
{isPending
|
||||
? "Transcending..."
|
||||
: `🌌 Transcend (+${formatInteger(echoPreview)} Echoes)`}
|
||||
: `🌌 Transcend (+${formatInteger(echoPreview, numberFormat)} Echoes)`}
|
||||
</button>
|
||||
{error === null
|
||||
? null
|
||||
@@ -248,7 +248,7 @@ const TranscendencePanel = (): JSX.Element => {
|
||||
: <p className="success">
|
||||
{"Transcended! Earned "}
|
||||
<strong>
|
||||
{formatInteger(result.echoes)}
|
||||
{formatInteger(result.echoes, numberFormat)}
|
||||
{" Echoes"}
|
||||
</strong>
|
||||
{". This is Transcendence "}
|
||||
@@ -266,7 +266,7 @@ const TranscendencePanel = (): JSX.Element => {
|
||||
<p className="shop-balance">
|
||||
{"Balance: "}
|
||||
<strong>
|
||||
{formatInteger(currentEchoes)}
|
||||
{formatInteger(currentEchoes, numberFormat)}
|
||||
{" Echoes"}
|
||||
</strong>
|
||||
</p>
|
||||
@@ -314,7 +314,7 @@ const TranscendencePanel = (): JSX.Element => {
|
||||
<p className="upgrade-cost">
|
||||
{purchased
|
||||
? "✅ Purchased"
|
||||
: `✨ ${formatInteger(upgrade.cost)} Echoes`}
|
||||
: `✨ ${formatInteger(upgrade.cost, numberFormat)} Echoes`}
|
||||
</p>
|
||||
</div>
|
||||
{purchased
|
||||
|
||||
@@ -82,7 +82,7 @@ const ResourceBar = ({
|
||||
isSyncing,
|
||||
onForceSync,
|
||||
}: ResourceBarProperties): JSX.Element => {
|
||||
const { formatInteger, formatNumber, syncError, state } = useGame();
|
||||
const { formatInteger, formatNumber, syncError, state, numberFormat } = useGame();
|
||||
const [ isProfileOpen, setIsProfileOpen ] = useState(false);
|
||||
const [ isResourcesOpen, setIsResourcesOpen ] = useState(false);
|
||||
|
||||
@@ -233,14 +233,14 @@ const ResourceBar = ({
|
||||
<div className="resource">
|
||||
<span className="resource-icon">{"🔮"}</span>
|
||||
<span className="resource-value">
|
||||
{formatInteger(runestones)}
|
||||
{formatInteger(runestones, numberFormat)}
|
||||
</span>
|
||||
<span className="resource-label">{"Runestones"}</span>
|
||||
</div>
|
||||
<div className="resource">
|
||||
<span className="resource-icon">{"⭐"}</span>
|
||||
<span className="resource-value">
|
||||
{`+${formatInteger(projectedRunestones)}`}
|
||||
{`+${formatInteger(projectedRunestones, numberFormat)}`}
|
||||
</span>
|
||||
<span className="resource-label">{"On Prestige"}</span>
|
||||
</div>
|
||||
|
||||
@@ -467,7 +467,7 @@ interface GameContextValue {
|
||||
/**
|
||||
* Format a whole-number value without decimal places.
|
||||
*/
|
||||
formatInteger: (value: number)=> string;
|
||||
formatInteger: (value: number, format: NumberFormat)=> string;
|
||||
|
||||
/**
|
||||
* Buy a prestige upgrade from the runestone shop.
|
||||
|
||||
@@ -112,29 +112,49 @@ const formatEngineering = (value: number): string => {
|
||||
* Formats a whole-number value for display without decimal places.
|
||||
* Uses the same suffix/letter logic as formatNumber but rounds to integers.
|
||||
* @param value - The integer value to format.
|
||||
* @param format
|
||||
* @returns The formatted string with no decimal places.
|
||||
*/
|
||||
const formatInteger = (value: number): string => {
|
||||
const formatInteger = (value: number,
|
||||
format: NumberFormat = "suffix"): string => {
|
||||
if (!Number.isFinite(value) || Number.isNaN(value)) {
|
||||
return "0";
|
||||
}
|
||||
if (value < 0) {
|
||||
return `-${formatInteger(-value)}`;
|
||||
}
|
||||
if (value >= Math.pow(10, letterBaseExp)) {
|
||||
const exp = Math.floor(Math.log10(value));
|
||||
const stepsAboveBase = Math.floor((exp - letterBaseExp) / 3);
|
||||
const steps = stepsAboveBase * 3;
|
||||
const divisorExp = letterBaseExp + steps;
|
||||
const divisor = Math.pow(10, divisorExp);
|
||||
return `${String(Math.round(value / divisor))}${getLetterSuffix(stepsAboveBase)}`;
|
||||
}
|
||||
for (const { threshold, suffix } of namedSuffixes) {
|
||||
if (value >= threshold) {
|
||||
return `${String(Math.floor(value / threshold))}${suffix}`;
|
||||
|
||||
const flooredValue = Math.floor(value);
|
||||
switch (format) {
|
||||
case "scientific":
|
||||
return formatScientific(flooredValue);
|
||||
case "engineering":
|
||||
return formatEngineering(flooredValue);
|
||||
case "suffix":
|
||||
return formatSuffix(flooredValue);
|
||||
default: {
|
||||
/* V8 ignore next -- @preserve */
|
||||
return formatSuffix(flooredValue);
|
||||
}
|
||||
}
|
||||
return String(Math.floor(value));
|
||||
*If (value >= Math.pow(10, letterBaseExp)) {
|
||||
* const exp = Math.floor(Math.log10(value));
|
||||
* const stepsAboveBase = Math.floor((exp - letterBaseExp) / 3);
|
||||
* const steps = stepsAboveBase * 3;
|
||||
* const divisorExp = letterBaseExp + steps;
|
||||
* const divisor = Math.pow(10, divisorExp);
|
||||
* return `${String(Math.round(value / divisor))}${getLetterSuffix(stepsAboveBase)}`;
|
||||
*}
|
||||
*
|
||||
*
|
||||
*
|
||||
*for (const { threshold, suffix } of namedSuffixes) {
|
||||
* if (value >= threshold) {
|
||||
* return `${String(Math.floor(value / threshold))}${suffix}`;
|
||||
* }
|
||||
*}
|
||||
*return String(Math.floor(value));
|
||||
*/
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable max-lines -- Test suites naturally have many cases */
|
||||
/* eslint-disable max-lines-per-function -- Test suites naturally have many cases */
|
||||
/* eslint-disable max-nested-callbacks -- Vitest structure requires nesting */
|
||||
/**
|
||||
@@ -197,4 +196,109 @@ describe("formatInteger", () => {
|
||||
expect(formatInteger(1e39)).toBe("1b");
|
||||
});
|
||||
});
|
||||
|
||||
describe("suffix format (default)", () => {
|
||||
it("should format small numbers with one decimal place", () => {
|
||||
expect(formatInteger(999)).toBe("999.0");
|
||||
});
|
||||
|
||||
it("should format thousands with K suffix", () => {
|
||||
expect(formatInteger(1000)).toBe("1.00K");
|
||||
});
|
||||
|
||||
it("should format millions with M suffix", () => {
|
||||
expect(formatInteger(1_000_000)).toBe("1.00M");
|
||||
});
|
||||
|
||||
it("should format billions with B suffix", () => {
|
||||
expect(formatInteger(1_000_000_000)).toBe("1.00B");
|
||||
});
|
||||
|
||||
it("should format trillions with T suffix", () => {
|
||||
expect(formatInteger(1e12)).toBe("1.00T");
|
||||
});
|
||||
|
||||
it("should format quadrillions with Qa suffix", () => {
|
||||
expect(formatInteger(1e15)).toBe("1.00Qa");
|
||||
});
|
||||
|
||||
it("should format quintillions with Qi suffix", () => {
|
||||
expect(formatInteger(1e18)).toBe("1.00Qi");
|
||||
});
|
||||
|
||||
it("should format sextillions with Sx suffix", () => {
|
||||
expect(formatInteger(1e21)).toBe("1.00Sx");
|
||||
});
|
||||
|
||||
it("should format septillions with Sp suffix", () => {
|
||||
expect(formatInteger(1e24)).toBe("1.00Sp");
|
||||
});
|
||||
|
||||
it("should format octillions with Oc suffix", () => {
|
||||
expect(formatInteger(1e27)).toBe("1.00Oc");
|
||||
});
|
||||
|
||||
it("should format nonillions with No suffix", () => {
|
||||
expect(formatInteger(1e30)).toBe("1.00No");
|
||||
});
|
||||
|
||||
it("should format decillions with Dc suffix", () => {
|
||||
expect(formatInteger(1e33)).toBe("1.00Dc");
|
||||
});
|
||||
|
||||
it("should format values >= 1e36 with letter suffix 'a'", () => {
|
||||
expect(formatInteger(1e36)).toBe("1.00a");
|
||||
});
|
||||
|
||||
it("should format values >= 1e39 with letter suffix 'b'", () => {
|
||||
expect(formatInteger(1e39)).toBe("1.00b");
|
||||
});
|
||||
|
||||
it("should format values at 26th letter step with 'z'", () => {
|
||||
expect(formatInteger(1e36 * Math.pow(10, 25 * 3))).toBe("1.00z");
|
||||
});
|
||||
|
||||
it("should format values at 27th letter step with 'aa'", () => {
|
||||
expect(formatInteger(1e36 * Math.pow(10, 26 * 3))).toBe("1.00aa");
|
||||
});
|
||||
});
|
||||
|
||||
describe("scientific format", () => {
|
||||
it("should fall back to suffix format below 1e6", () => {
|
||||
expect(formatInteger(500, "scientific")).toBe("500.0");
|
||||
});
|
||||
|
||||
it("should format values >= 1e6 in scientific notation", () => {
|
||||
expect(formatInteger(1_230_000, "scientific")).toBe("1.23e6");
|
||||
});
|
||||
|
||||
it("should format large values in scientific notation", () => {
|
||||
expect(formatInteger(1e18, "scientific")).toBe("1.00e18");
|
||||
});
|
||||
});
|
||||
|
||||
describe("engineering format", () => {
|
||||
it("should fall back to suffix format below 1e6", () => {
|
||||
expect(formatInteger(500, "engineering")).toBe("500.0");
|
||||
});
|
||||
|
||||
it("should format values >= 1e6 with exponent multiple of 3", () => {
|
||||
expect(formatInteger(1_230_000, "engineering")).toBe("1.23E6");
|
||||
});
|
||||
|
||||
it("should format 1e9 correctly in engineering notation", () => {
|
||||
expect(formatInteger(1e9, "engineering")).toBe("1.00E9");
|
||||
});
|
||||
|
||||
it("should format 12350000 correctly in engineering notation", () => {
|
||||
expect(formatInteger(12_350_000, "engineering")).toBe("12.35E6");
|
||||
});
|
||||
});
|
||||
|
||||
describe("unknown format (default branch)", () => {
|
||||
it("should fall back to suffix format for an unrecognised format string", () => {
|
||||
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Testing unreachable default branch */
|
||||
expect(formatInteger(1000, "unknown" as never)).toBe("1.00K");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user