feat: add number format config, resource cap, and modal scroll fix

- Add user-configurable number format (suffix/scientific/engineering)
  - Suffix: K/M/B/T through Dc (1e33), then letter-based a/b/c... indefinitely
  - Scientific: 1.23e15 style via toExponential
  - Engineering: exponent always a multiple of 3 (1.23E15)
  - Stored in ProfileSettings, fetched from profile API on load
  - Picker UI in EditProfileModal with live examples
- Cap all resource accumulation at 1e300 (RESOURCE_CAP constant)
  - Per-resource FULL badge with tooltip in ResourceBar
  - Amber notice strip when any resource is at cap
  - handleClick also respects the cap
- Make EditProfileModal scrollable with viewport margin
  - Flex column layout with sticky header, scrollable form body
  - Bio textarea preserved as resizable with min-height
- Fix ReferenceError: formatNumber not defined in BossPanel/AchievementPanel
  - Pass formatNumber as prop to BossCard and AchievementCard
  - Pass formatNumber as parameter to conditionDescription
This commit is contained in:
2026-03-06 18:59:43 -08:00
committed by Naomi Carrigan
parent 24beaf3131
commit 5ad2c44399
15 changed files with 290 additions and 65 deletions
+25 -7
View File
@@ -1,5 +1,6 @@
import type { Resource } from "@elysium/types";
import { formatNumber } from "../../utils/format.js";
import { useGame } from "../../context/GameContext.js";
import { RESOURCE_CAP } from "../../engine/tick.js";
interface ResourceBarProps {
resources: Resource;
@@ -21,6 +22,8 @@ const formatRelativeTime = (timestamp: number): string => {
return `${hours}h ago`;
};
const RESOURCE_FULL_TOOLTIP = "This resource is full! Consider spending some or prestiging to keep earning.";
export const ResourceBar = ({
resources,
prestigeCount,
@@ -29,27 +32,35 @@ export const ResourceBar = ({
lastSavedAt,
isSyncing,
onForceSync,
}: ResourceBarProps): React.JSX.Element => (
}: ResourceBarProps): React.JSX.Element => {
const { formatNumber } = useGame();
const anyFull = Object.values(resources).some((v) => v >= RESOURCE_CAP);
return (
<>
<header className="resource-bar">
<div className="resource">
<div className={`resource${resources.gold >= RESOURCE_CAP ? " resource-full" : ""}`}>
<span className="resource-icon">🪙</span>
<span className="resource-value">{formatNumber(resources.gold)}</span>
<span className="resource-label">Gold</span>
{resources.gold >= RESOURCE_CAP && <span className="resource-cap-badge" title={RESOURCE_FULL_TOOLTIP}>FULL</span>}
</div>
<div className="resource">
<div className={`resource${resources.essence >= RESOURCE_CAP ? " resource-full" : ""}`}>
<span className="resource-icon"></span>
<span className="resource-value">{formatNumber(resources.essence)}</span>
<span className="resource-label">Essence</span>
{resources.essence >= RESOURCE_CAP && <span className="resource-cap-badge" title={RESOURCE_FULL_TOOLTIP}>FULL</span>}
</div>
<div className="resource">
<div className={`resource${resources.crystals >= RESOURCE_CAP ? " resource-full" : ""}`}>
<span className="resource-icon">💎</span>
<span className="resource-value">{formatNumber(resources.crystals)}</span>
<span className="resource-label">Crystals</span>
{resources.crystals >= RESOURCE_CAP && <span className="resource-cap-badge" title={RESOURCE_FULL_TOOLTIP}>FULL</span>}
</div>
<div className="resource">
<div className={`resource${resources.runestones >= RESOURCE_CAP ? " resource-full" : ""}`}>
<span className="resource-icon">🔮</span>
<span className="resource-value">{formatNumber(resources.runestones)}</span>
<span className="resource-label">Runestones</span>
{resources.runestones >= RESOURCE_CAP && <span className="resource-cap-badge" title={RESOURCE_FULL_TOOLTIP}>FULL</span>}
</div>
{prestigeCount > 0 && (
<div className="prestige-badge">
@@ -90,4 +101,11 @@ export const ResourceBar = ({
</button>
</div>
</header>
);
{anyFull && (
<div className="resource-cap-notice">
One or more resources are full! Consider spending some or prestiging to keep earning.
</div>
)}
</>
);
};