generated from nhcarrigan/template
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:
@@ -10,9 +10,14 @@ import { authMiddleware } from "../middleware/auth.js";
|
|||||||
|
|
||||||
export const profileRouter = new Hono();
|
export const profileRouter = new Hono();
|
||||||
|
|
||||||
|
const VALID_NUMBER_FORMATS = new Set(["suffix", "scientific", "engineering"]);
|
||||||
|
|
||||||
const parseProfileSettings = (raw: unknown): ProfileSettings => {
|
const parseProfileSettings = (raw: unknown): ProfileSettings => {
|
||||||
if (raw !== null && typeof raw === "object" && !Array.isArray(raw)) {
|
if (raw !== null && typeof raw === "object" && !Array.isArray(raw)) {
|
||||||
const obj = raw as Record<string, unknown>;
|
const obj = raw as Record<string, unknown>;
|
||||||
|
const numberFormat = VALID_NUMBER_FORMATS.has(obj.numberFormat as string)
|
||||||
|
? (obj.numberFormat as ProfileSettings["numberFormat"])
|
||||||
|
: "suffix";
|
||||||
return {
|
return {
|
||||||
showTotalGold: obj.showTotalGold !== false,
|
showTotalGold: obj.showTotalGold !== false,
|
||||||
showTotalClicks: obj.showTotalClicks !== false,
|
showTotalClicks: obj.showTotalClicks !== false,
|
||||||
@@ -22,6 +27,7 @@ const parseProfileSettings = (raw: unknown): ProfileSettings => {
|
|||||||
showQuestsCompleted: obj.showQuestsCompleted !== false,
|
showQuestsCompleted: obj.showQuestsCompleted !== false,
|
||||||
showAdventurersRecruited: obj.showAdventurersRecruited !== false,
|
showAdventurersRecruited: obj.showAdventurersRecruited !== false,
|
||||||
showAchievementsUnlocked: obj.showAchievementsUnlocked !== false,
|
showAchievementsUnlocked: obj.showAchievementsUnlocked !== false,
|
||||||
|
numberFormat,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return { ...DEFAULT_PROFILE_SETTINGS };
|
return { ...DEFAULT_PROFILE_SETTINGS };
|
||||||
@@ -73,6 +79,9 @@ profileRouter.put("/", authMiddleware, async (context) => {
|
|||||||
|
|
||||||
const characterName = (body.characterName ?? "").trim().slice(0, 32);
|
const characterName = (body.characterName ?? "").trim().slice(0, 32);
|
||||||
const bio = (body.bio ?? "").trim().slice(0, 200);
|
const bio = (body.bio ?? "").trim().slice(0, 200);
|
||||||
|
const numberFormat = VALID_NUMBER_FORMATS.has(body.profileSettings?.numberFormat as string)
|
||||||
|
? (body.profileSettings?.numberFormat as ProfileSettings["numberFormat"])
|
||||||
|
: "suffix";
|
||||||
const profileSettings: ProfileSettings = {
|
const profileSettings: ProfileSettings = {
|
||||||
showTotalGold: body.profileSettings?.showTotalGold !== false,
|
showTotalGold: body.profileSettings?.showTotalGold !== false,
|
||||||
showTotalClicks: body.profileSettings?.showTotalClicks !== false,
|
showTotalClicks: body.profileSettings?.showTotalClicks !== false,
|
||||||
@@ -82,6 +91,7 @@ profileRouter.put("/", authMiddleware, async (context) => {
|
|||||||
showQuestsCompleted: body.profileSettings?.showQuestsCompleted !== false,
|
showQuestsCompleted: body.profileSettings?.showQuestsCompleted !== false,
|
||||||
showAdventurersRecruited: body.profileSettings?.showAdventurersRecruited !== false,
|
showAdventurersRecruited: body.profileSettings?.showAdventurersRecruited !== false,
|
||||||
showAchievementsUnlocked: body.profileSettings?.showAchievementsUnlocked !== false,
|
showAchievementsUnlocked: body.profileSettings?.showAchievementsUnlocked !== false,
|
||||||
|
numberFormat,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!characterName) {
|
if (!characterName) {
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import type { Achievement } from "@elysium/types";
|
import type { Achievement } from "@elysium/types";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useGame } from "../../context/GameContext.js";
|
import { useGame } from "../../context/GameContext.js";
|
||||||
import { formatNumber } from "../../utils/format.js";
|
|
||||||
import { LockToggle } from "../ui/LockToggle.js";
|
import { LockToggle } from "../ui/LockToggle.js";
|
||||||
|
|
||||||
const conditionDescription = (achievement: Achievement): string => {
|
const conditionDescription = (achievement: Achievement, formatNumber: (n: number) => string): string => {
|
||||||
const { condition } = achievement;
|
const { condition } = achievement;
|
||||||
switch (condition.type) {
|
switch (condition.type) {
|
||||||
case "totalGoldEarned":
|
case "totalGoldEarned":
|
||||||
@@ -26,9 +25,10 @@ const conditionDescription = (achievement: Achievement): string => {
|
|||||||
|
|
||||||
interface AchievementCardProps {
|
interface AchievementCardProps {
|
||||||
achievement: Achievement;
|
achievement: Achievement;
|
||||||
|
formatNumber: (n: number) => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AchievementCard = ({ achievement }: AchievementCardProps): React.JSX.Element => {
|
const AchievementCard = ({ achievement, formatNumber }: AchievementCardProps): React.JSX.Element => {
|
||||||
const isUnlocked = achievement.unlockedAt !== null;
|
const isUnlocked = achievement.unlockedAt !== null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -37,7 +37,7 @@ const AchievementCard = ({ achievement }: AchievementCardProps): React.JSX.Eleme
|
|||||||
<div className="achievement-info">
|
<div className="achievement-info">
|
||||||
<h3>{achievement.name}</h3>
|
<h3>{achievement.name}</h3>
|
||||||
<p>{achievement.description}</p>
|
<p>{achievement.description}</p>
|
||||||
<p className="achievement-condition">{conditionDescription(achievement)}</p>
|
<p className="achievement-condition">{conditionDescription(achievement, formatNumber)}</p>
|
||||||
{achievement.reward?.crystals != null && (
|
{achievement.reward?.crystals != null && (
|
||||||
<p className="achievement-reward">💎 +{achievement.reward.crystals} Crystals</p>
|
<p className="achievement-reward">💎 +{achievement.reward.crystals} Crystals</p>
|
||||||
)}
|
)}
|
||||||
@@ -54,7 +54,7 @@ const AchievementCard = ({ achievement }: AchievementCardProps): React.JSX.Eleme
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const AchievementPanel = (): React.JSX.Element => {
|
export const AchievementPanel = (): React.JSX.Element => {
|
||||||
const { state } = useGame();
|
const { state, formatNumber } = useGame();
|
||||||
const [showLocked, setShowLocked] = useState(true);
|
const [showLocked, setShowLocked] = useState(true);
|
||||||
|
|
||||||
if (!state) return <section className="panel"><p>Loading...</p></section>;
|
if (!state) return <section className="panel"><p>Loading...</p></section>;
|
||||||
@@ -79,7 +79,7 @@ export const AchievementPanel = (): React.JSX.Element => {
|
|||||||
</p>
|
</p>
|
||||||
<div className="achievement-list">
|
<div className="achievement-list">
|
||||||
{visible.map((achievement) => (
|
{visible.map((achievement) => (
|
||||||
<AchievementCard key={achievement.id} achievement={achievement} />
|
<AchievementCard key={achievement.id} achievement={achievement} formatNumber={formatNumber} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { BattleResult } from "../../context/GameContext.js";
|
import type { BattleResult } from "../../context/GameContext.js";
|
||||||
|
import { useGame } from "../../context/GameContext.js";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
interface BattleModalProps {
|
interface BattleModalProps {
|
||||||
@@ -6,17 +7,12 @@ interface BattleModalProps {
|
|||||||
onDismiss: () => void;
|
onDismiss: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatNumber = (n: number): string => {
|
|
||||||
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
|
||||||
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`;
|
|
||||||
return Math.floor(n).toLocaleString();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const BattleModal = ({
|
export const BattleModal = ({
|
||||||
battle,
|
battle,
|
||||||
onDismiss,
|
onDismiss,
|
||||||
}: BattleModalProps): React.JSX.Element => {
|
}: BattleModalProps): React.JSX.Element => {
|
||||||
const { result, bossName } = battle;
|
const { result, bossName } = battle;
|
||||||
|
const { formatNumber } = useGame();
|
||||||
|
|
||||||
const [phase, setPhase] = useState<"animating" | "result">("animating");
|
const [phase, setPhase] = useState<"animating" | "result">("animating");
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import type { Boss } from "@elysium/types";
|
import type { Boss } from "@elysium/types";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useGame } from "../../context/GameContext.js";
|
import { useGame } from "../../context/GameContext.js";
|
||||||
import { formatNumber } from "../../utils/format.js";
|
|
||||||
import { LockToggle } from "../ui/LockToggle.js";
|
import { LockToggle } from "../ui/LockToggle.js";
|
||||||
import { ZoneSelector } from "./ZoneSelector.js";
|
import { ZoneSelector } from "./ZoneSelector.js";
|
||||||
|
|
||||||
@@ -11,6 +10,7 @@ interface BossCardProps {
|
|||||||
onChallenge: (bossId: string) => void;
|
onChallenge: (bossId: string) => void;
|
||||||
isChallenging: boolean;
|
isChallenging: boolean;
|
||||||
unlockHint?: string | undefined;
|
unlockHint?: string | undefined;
|
||||||
|
formatNumber: (n: number) => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BossCard = ({
|
const BossCard = ({
|
||||||
@@ -19,6 +19,7 @@ const BossCard = ({
|
|||||||
onChallenge,
|
onChallenge,
|
||||||
isChallenging,
|
isChallenging,
|
||||||
unlockHint,
|
unlockHint,
|
||||||
|
formatNumber,
|
||||||
}: BossCardProps): React.JSX.Element => {
|
}: BossCardProps): React.JSX.Element => {
|
||||||
const hpPercent = (boss.currentHp / boss.maxHp) * 100;
|
const hpPercent = (boss.currentHp / boss.maxHp) * 100;
|
||||||
const isPrestigeLocked = boss.prestigeRequirement > prestigeCount;
|
const isPrestigeLocked = boss.prestigeRequirement > prestigeCount;
|
||||||
@@ -92,7 +93,7 @@ const BossCard = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const BossPanel = (): React.JSX.Element => {
|
export const BossPanel = (): React.JSX.Element => {
|
||||||
const { state, challengeBoss } = useGame();
|
const { state, challengeBoss, formatNumber } = useGame();
|
||||||
const [challengingBossId, setChallengingBossId] = useState<string | null>(null);
|
const [challengingBossId, setChallengingBossId] = useState<string | null>(null);
|
||||||
const [activeZoneId, setActiveZoneId] = useState("verdant_vale");
|
const [activeZoneId, setActiveZoneId] = useState("verdant_vale");
|
||||||
const [showLocked, setShowLocked] = useState(true);
|
const [showLocked, setShowLocked] = useState(true);
|
||||||
@@ -212,6 +213,7 @@ export const BossPanel = (): React.JSX.Element => {
|
|||||||
<BossCard
|
<BossCard
|
||||||
key={boss.id}
|
key={boss.id}
|
||||||
boss={boss}
|
boss={boss}
|
||||||
|
formatNumber={formatNumber}
|
||||||
isChallenging={challengingBossId === boss.id}
|
isChallenging={challengingBossId === boss.id}
|
||||||
prestigeCount={state.prestige.count}
|
prestigeCount={state.prestige.count}
|
||||||
unlockHint={bossUnlockHints.get(boss.id)}
|
unlockHint={bossUnlockHints.get(boss.id)}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useCallback, useRef, useState } from "react";
|
import { useCallback, useRef, useState } from "react";
|
||||||
import { useGame } from "../../context/GameContext.js";
|
import { useGame } from "../../context/GameContext.js";
|
||||||
import { calculateClickPower } from "../../engine/tick.js";
|
import { calculateClickPower } from "../../engine/tick.js";
|
||||||
import { formatNumber } from "../../utils/format.js";
|
|
||||||
|
|
||||||
interface FloatText {
|
interface FloatText {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -11,7 +10,7 @@ interface FloatText {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ClickArea = (): React.JSX.Element => {
|
export const ClickArea = (): React.JSX.Element => {
|
||||||
const { state, handleClick } = useGame();
|
const { state, handleClick, formatNumber } = useGame();
|
||||||
const [floats, setFloats] = useState<FloatText[]>([]);
|
const [floats, setFloats] = useState<FloatText[]>([]);
|
||||||
const nextIdRef = useRef(0);
|
const nextIdRef = useRef(0);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ProfileSettings } from "@elysium/types";
|
import type { NumberFormat, ProfileSettings } from "@elysium/types";
|
||||||
import { DEFAULT_PROFILE_SETTINGS } from "@elysium/types";
|
import { DEFAULT_PROFILE_SETTINGS } from "@elysium/types";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { updateProfile } from "../../api/client.js";
|
import { updateProfile } from "../../api/client.js";
|
||||||
@@ -20,12 +20,15 @@ const STAT_TOGGLES: { key: keyof ProfileSettings; label: string; icon: string }[
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const EditProfileModal = ({ onClose }: EditProfileModalProps): React.JSX.Element => {
|
export const EditProfileModal = ({ onClose }: EditProfileModalProps): React.JSX.Element => {
|
||||||
const { state } = useGame();
|
const { state, numberFormat: currentNumberFormat, setNumberFormat } = useGame();
|
||||||
const player = state?.player;
|
const player = state?.player;
|
||||||
|
|
||||||
const [characterName, setCharacterName] = useState(player?.characterName ?? "");
|
const [characterName, setCharacterName] = useState(player?.characterName ?? "");
|
||||||
const [bio, setBio] = useState("");
|
const [bio, setBio] = useState("");
|
||||||
const [settings, setSettings] = useState<ProfileSettings>({ ...DEFAULT_PROFILE_SETTINGS });
|
const [settings, setSettings] = useState<ProfileSettings>({
|
||||||
|
...DEFAULT_PROFILE_SETTINGS,
|
||||||
|
numberFormat: currentNumberFormat,
|
||||||
|
});
|
||||||
const [loadingProfile, setLoadingProfile] = useState(true);
|
const [loadingProfile, setLoadingProfile] = useState(true);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@@ -59,6 +62,7 @@ export const EditProfileModal = ({ onClose }: EditProfileModalProps): React.JSX.
|
|||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
await updateProfile({ characterName, bio, profileSettings: settings });
|
await updateProfile({ characterName, bio, profileSettings: settings });
|
||||||
|
setNumberFormat(settings.numberFormat);
|
||||||
setSaved(true);
|
setSaved(true);
|
||||||
setTimeout(onClose, 900);
|
setTimeout(onClose, 900);
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
@@ -139,6 +143,30 @@ export const EditProfileModal = ({ onClose }: EditProfileModalProps): React.JSX.
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="edit-profile-section">
|
||||||
|
<p className="edit-profile-label">Number Format</p>
|
||||||
|
<p className="edit-profile-sublabel">How large numbers appear across the game.</p>
|
||||||
|
<div className="number-format-picker">
|
||||||
|
{(
|
||||||
|
[
|
||||||
|
{ value: "suffix", label: "Suffix", example: "1.23Qa" },
|
||||||
|
{ value: "scientific", label: "Scientific", example: "1.23e15" },
|
||||||
|
{ value: "engineering", label: "Engineering", example: "1.23E15" },
|
||||||
|
] as { value: NumberFormat; label: string; example: string }[]
|
||||||
|
).map(({ value, label, example }) => (
|
||||||
|
<button
|
||||||
|
key={value}
|
||||||
|
className={`number-format-btn ${settings.numberFormat === value ? "number-format-active" : ""}`}
|
||||||
|
onClick={() => { setSettings((prev) => ({ ...prev, numberFormat: value })); }}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span className="number-format-label">{label}</span>
|
||||||
|
<span className="number-format-example">{example}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{error && <p className="edit-profile-error">{error}</p>}
|
{error && <p className="edit-profile-error">{error}</p>}
|
||||||
|
|
||||||
<div className="edit-profile-actions">
|
<div className="edit-profile-actions">
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import type { PublicProfileResponse } from "@elysium/types";
|
import type { PublicProfileResponse } from "@elysium/types";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { formatNumber } from "../../utils/format.js";
|
import { useGame } from "../../context/GameContext.js";
|
||||||
|
|
||||||
interface ProfilePageProps {
|
interface ProfilePageProps {
|
||||||
discordId: string;
|
discordId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProfilePage = ({ discordId }: ProfilePageProps): React.JSX.Element => {
|
export const ProfilePage = ({ discordId }: ProfilePageProps): React.JSX.Element => {
|
||||||
|
const { formatNumber } = useGame();
|
||||||
const [profile, setProfile] = useState<PublicProfileResponse | null>(null);
|
const [profile, setProfile] = useState<PublicProfileResponse | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import type { Quest } from "@elysium/types";
|
import type { Quest } from "@elysium/types";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useGame } from "../../context/GameContext.js";
|
import { useGame } from "../../context/GameContext.js";
|
||||||
import { formatNumber } from "../../utils/format.js";
|
|
||||||
import { LockToggle } from "../ui/LockToggle.js";
|
import { LockToggle } from "../ui/LockToggle.js";
|
||||||
import { ZoneSelector } from "./ZoneSelector.js";
|
import { ZoneSelector } from "./ZoneSelector.js";
|
||||||
|
|
||||||
@@ -24,7 +23,7 @@ interface QuestCardProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const QuestCard = ({ quest, unlockHint, zoneHint }: QuestCardProps): React.JSX.Element => {
|
const QuestCard = ({ quest, unlockHint, zoneHint }: QuestCardProps): React.JSX.Element => {
|
||||||
const { startQuest } = useGame();
|
const { startQuest, formatNumber } = useGame();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`quest-card quest-${quest.status}`}>
|
<div className={`quest-card quest-${quest.status}`}>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { Resource } from "@elysium/types";
|
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 {
|
interface ResourceBarProps {
|
||||||
resources: Resource;
|
resources: Resource;
|
||||||
@@ -21,6 +22,8 @@ const formatRelativeTime = (timestamp: number): string => {
|
|||||||
return `${hours}h ago`;
|
return `${hours}h ago`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const RESOURCE_FULL_TOOLTIP = "This resource is full! Consider spending some or prestiging to keep earning.";
|
||||||
|
|
||||||
export const ResourceBar = ({
|
export const ResourceBar = ({
|
||||||
resources,
|
resources,
|
||||||
prestigeCount,
|
prestigeCount,
|
||||||
@@ -29,27 +32,35 @@ export const ResourceBar = ({
|
|||||||
lastSavedAt,
|
lastSavedAt,
|
||||||
isSyncing,
|
isSyncing,
|
||||||
onForceSync,
|
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">
|
<header className="resource-bar">
|
||||||
<div className="resource">
|
<div className={`resource${resources.gold >= RESOURCE_CAP ? " resource-full" : ""}`}>
|
||||||
<span className="resource-icon">🪙</span>
|
<span className="resource-icon">🪙</span>
|
||||||
<span className="resource-value">{formatNumber(resources.gold)}</span>
|
<span className="resource-value">{formatNumber(resources.gold)}</span>
|
||||||
<span className="resource-label">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>
|
||||||
<div className="resource">
|
<div className={`resource${resources.essence >= RESOURCE_CAP ? " resource-full" : ""}`}>
|
||||||
<span className="resource-icon">✨</span>
|
<span className="resource-icon">✨</span>
|
||||||
<span className="resource-value">{formatNumber(resources.essence)}</span>
|
<span className="resource-value">{formatNumber(resources.essence)}</span>
|
||||||
<span className="resource-label">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>
|
||||||
<div className="resource">
|
<div className={`resource${resources.crystals >= RESOURCE_CAP ? " resource-full" : ""}`}>
|
||||||
<span className="resource-icon">💎</span>
|
<span className="resource-icon">💎</span>
|
||||||
<span className="resource-value">{formatNumber(resources.crystals)}</span>
|
<span className="resource-value">{formatNumber(resources.crystals)}</span>
|
||||||
<span className="resource-label">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>
|
||||||
<div className="resource">
|
<div className={`resource${resources.runestones >= RESOURCE_CAP ? " resource-full" : ""}`}>
|
||||||
<span className="resource-icon">🔮</span>
|
<span className="resource-icon">🔮</span>
|
||||||
<span className="resource-value">{formatNumber(resources.runestones)}</span>
|
<span className="resource-value">{formatNumber(resources.runestones)}</span>
|
||||||
<span className="resource-label">Runestones</span>
|
<span className="resource-label">Runestones</span>
|
||||||
|
{resources.runestones >= RESOURCE_CAP && <span className="resource-cap-badge" title={RESOURCE_FULL_TOOLTIP}>FULL</span>}
|
||||||
</div>
|
</div>
|
||||||
{prestigeCount > 0 && (
|
{prestigeCount > 0 && (
|
||||||
<div className="prestige-badge">
|
<div className="prestige-badge">
|
||||||
@@ -90,4 +101,11 @@ export const ResourceBar = ({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
{anyFull && (
|
||||||
|
<div className="resource-cap-notice">
|
||||||
|
⚠️ One or more resources are full! Consider spending some or prestiging to keep earning.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Achievement, BossChallengeResponse, GameState } from "@elysium/types";
|
import type { Achievement, BossChallengeResponse, GameState, NumberFormat } from "@elysium/types";
|
||||||
import {
|
import {
|
||||||
createContext,
|
createContext,
|
||||||
useCallback,
|
useCallback,
|
||||||
@@ -8,7 +8,8 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { challengeBoss as challengeBossApi, loadGame, saveGame } from "../api/client.js";
|
import { challengeBoss as challengeBossApi, loadGame, saveGame } from "../api/client.js";
|
||||||
import { applyTick, calculateClickPower } from "../engine/tick.js";
|
import { RESOURCE_CAP, applyTick, calculateClickPower } from "../engine/tick.js";
|
||||||
|
import { formatNumber as formatNumberUtil } from "../utils/format.js";
|
||||||
|
|
||||||
|
|
||||||
export interface BattleResult {
|
export interface BattleResult {
|
||||||
@@ -54,6 +55,12 @@ interface GameContextValue {
|
|||||||
newAchievements: Achievement[];
|
newAchievements: Achievement[];
|
||||||
/** Remove an achievement from the toast queue */
|
/** Remove an achievement from the toast queue */
|
||||||
dismissAchievement: (id: string) => void;
|
dismissAchievement: (id: string) => void;
|
||||||
|
/** The player's chosen number display format */
|
||||||
|
numberFormat: NumberFormat;
|
||||||
|
/** Update the number format preference (persisted to server via profile save) */
|
||||||
|
setNumberFormat: (format: NumberFormat) => void;
|
||||||
|
/** Format a number using the player's chosen notation style */
|
||||||
|
formatNumber: (value: number) => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const GameContext = createContext<GameContextValue | null>(null);
|
const GameContext = createContext<GameContextValue | null>(null);
|
||||||
@@ -69,6 +76,7 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
|
|||||||
const [newAchievements, setNewAchievements] = useState<Achievement[]>([]);
|
const [newAchievements, setNewAchievements] = useState<Achievement[]>([]);
|
||||||
const [lastSavedAt, setLastSavedAt] = useState<number | null>(null);
|
const [lastSavedAt, setLastSavedAt] = useState<number | null>(null);
|
||||||
const [isSyncing, setIsSyncing] = useState(false);
|
const [isSyncing, setIsSyncing] = useState(false);
|
||||||
|
const [numberFormat, setNumberFormat] = useState<NumberFormat>("suffix");
|
||||||
const stateRef = useRef<GameState | null>(null);
|
const stateRef = useRef<GameState | null>(null);
|
||||||
const lastSaveRef = useRef<number>(Date.now());
|
const lastSaveRef = useRef<number>(Date.now());
|
||||||
const isSyncingRef = useRef(false);
|
const isSyncingRef = useRef(false);
|
||||||
@@ -87,6 +95,17 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
|
|||||||
if (data.offlineGold > 0) {
|
if (data.offlineGold > 0) {
|
||||||
setOfflineGold(data.offlineGold);
|
setOfflineGold(data.offlineGold);
|
||||||
}
|
}
|
||||||
|
// Fetch number format preference from profile (fire-and-forget, non-blocking)
|
||||||
|
void fetch(`/api/profile/${data.state.player.discordId}`)
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) return;
|
||||||
|
const profile = await res.json() as { profileSettings?: { numberFormat?: NumberFormat } };
|
||||||
|
const fmt = profile.profileSettings?.numberFormat;
|
||||||
|
if (fmt === "suffix" || fmt === "scientific" || fmt === "engineering") {
|
||||||
|
setNumberFormat(fmt);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => { /* fall back to default "suffix" */ });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : "Failed to load game");
|
setError(err instanceof Error ? err.message : "Failed to load game");
|
||||||
} finally {
|
} finally {
|
||||||
@@ -166,9 +185,10 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
|
|||||||
setState((prev) => {
|
setState((prev) => {
|
||||||
if (!prev) return prev;
|
if (!prev) return prev;
|
||||||
const clickPower = calculateClickPower(prev);
|
const clickPower = calculateClickPower(prev);
|
||||||
|
const newGold = Math.min(prev.resources.gold + clickPower, RESOURCE_CAP);
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
resources: { ...prev.resources, gold: prev.resources.gold + clickPower },
|
resources: { ...prev.resources, gold: newGold },
|
||||||
player: {
|
player: {
|
||||||
...prev.player,
|
...prev.player,
|
||||||
totalGoldEarned: prev.player.totalGoldEarned + clickPower,
|
totalGoldEarned: prev.player.totalGoldEarned + clickPower,
|
||||||
@@ -408,6 +428,11 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
|
|||||||
setNewAchievements((prev) => prev.filter((a) => a.id !== id));
|
setNewAchievements((prev) => prev.filter((a) => a.id !== id));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const boundFormatNumber = useCallback(
|
||||||
|
(value: number) => formatNumberUtil(value, numberFormat),
|
||||||
|
[numberFormat],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GameContext.Provider
|
<GameContext.Provider
|
||||||
value={{
|
value={{
|
||||||
@@ -431,6 +456,9 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
|
|||||||
dismissBattle,
|
dismissBattle,
|
||||||
newAchievements,
|
newAchievements,
|
||||||
dismissAchievement,
|
dismissAchievement,
|
||||||
|
numberFormat,
|
||||||
|
setNumberFormat,
|
||||||
|
formatNumber: boundFormatNumber,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -40,6 +40,11 @@ const checkAchievements = (state: GameState): Achievement[] => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Maximum value any resource can accumulate to. Beyond this JS floats lose all useful precision. */
|
||||||
|
export const RESOURCE_CAP = 1e300;
|
||||||
|
|
||||||
|
const capResource = (value: number): number => Math.min(value, RESOURCE_CAP);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pure function — applies one game tick to the state.
|
* Pure function — applies one game tick to the state.
|
||||||
* deltaSeconds: time elapsed since last tick.
|
* deltaSeconds: time elapsed since last tick.
|
||||||
@@ -187,8 +192,8 @@ export const applyTick = (state: GameState, deltaSeconds: number): GameState =>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const newGold = state.resources.gold + goldGained + questGold;
|
const newGold = capResource(state.resources.gold + goldGained + questGold);
|
||||||
const newEssence = state.resources.essence + essenceGained + questEssence;
|
const newEssence = capResource(state.resources.essence + essenceGained + questEssence);
|
||||||
const newTotalGoldEarned = state.player.totalGoldEarned + goldGained + questGold;
|
const newTotalGoldEarned = state.player.totalGoldEarned + goldGained + questGold;
|
||||||
|
|
||||||
const partialState: GameState = {
|
const partialState: GameState = {
|
||||||
@@ -197,7 +202,7 @@ export const applyTick = (state: GameState, deltaSeconds: number): GameState =>
|
|||||||
...state.resources,
|
...state.resources,
|
||||||
gold: newGold,
|
gold: newGold,
|
||||||
essence: newEssence,
|
essence: newEssence,
|
||||||
crystals: state.resources.crystals + questCrystals,
|
crystals: capResource(state.resources.crystals + questCrystals),
|
||||||
},
|
},
|
||||||
player: {
|
player: {
|
||||||
...state.player,
|
...state.player,
|
||||||
@@ -228,7 +233,7 @@ export const applyTick = (state: GameState, deltaSeconds: number): GameState =>
|
|||||||
achievements: updatedAchievements,
|
achievements: updatedAchievements,
|
||||||
resources: {
|
resources: {
|
||||||
...partialState.resources,
|
...partialState.resources,
|
||||||
crystals: partialState.resources.crystals + crystalsFromAchievements,
|
crystals: capResource(partialState.resources.crystals + crystalsFromAchievements),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
+79
-2
@@ -71,6 +71,30 @@ body {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.resource-full .resource-value {
|
||||||
|
color: var(--colour-warning, #f59e0b);
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-cap-badge {
|
||||||
|
background: var(--colour-warning, #f59e0b);
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
color: #000;
|
||||||
|
cursor: help;
|
||||||
|
font-size: 0.6rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
padding: 0.1rem 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-cap-notice {
|
||||||
|
background: rgba(245, 158, 11, 0.12);
|
||||||
|
border-bottom: 1px solid rgba(245, 158, 11, 0.35);
|
||||||
|
color: #f59e0b;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
padding: 0.4rem 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
/* ===================== GAME LAYOUT ===================== */
|
/* ===================== GAME LAYOUT ===================== */
|
||||||
.game-layout {
|
.game-layout {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -1366,7 +1390,11 @@ body {
|
|||||||
/* ── Edit Profile Modal ─────────────────────────────────────────────────── */
|
/* ── Edit Profile Modal ─────────────────────────────────────────────────── */
|
||||||
|
|
||||||
.edit-profile-modal {
|
.edit-profile-modal {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-height: calc(100dvh - 4rem);
|
||||||
max-width: 480px;
|
max-width: 480px;
|
||||||
|
padding: 0;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
@@ -1374,14 +1402,19 @@ body {
|
|||||||
.modal-header {
|
.modal-header {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-bottom: 1rem;
|
padding: 1.5rem 2rem 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-header h2 {
|
.modal-header h2 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.edit-profile-modal .edit-profile-loading {
|
||||||
|
padding: 0 2rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.modal-close {
|
.modal-close {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -1401,6 +1434,8 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0 2rem 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-profile-label {
|
.edit-profile-label {
|
||||||
@@ -1425,10 +1460,14 @@ body {
|
|||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 0.5rem 0.75rem;
|
||||||
resize: vertical;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.edit-profile-textarea {
|
||||||
|
min-height: 5rem;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
.edit-profile-input:focus,
|
.edit-profile-input:focus,
|
||||||
.edit-profile-textarea:focus {
|
.edit-profile-textarea:focus {
|
||||||
border-color: var(--colour-primary);
|
border-color: var(--colour-primary);
|
||||||
@@ -1484,6 +1523,44 @@ body {
|
|||||||
color: var(--colour-success);
|
color: var(--colour-success);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.number-format-picker {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.number-format-btn {
|
||||||
|
align-items: center;
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
border: 1px solid rgba(147, 51, 234, 0.25);
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
color: var(--colour-text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
font-family: inherit;
|
||||||
|
gap: 0.2rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.number-format-active {
|
||||||
|
background: rgba(147, 51, 234, 0.12);
|
||||||
|
border-color: rgba(147, 51, 234, 0.5);
|
||||||
|
color: var(--colour-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.number-format-label {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.number-format-example {
|
||||||
|
color: var(--colour-accent);
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
.edit-profile-error {
|
.edit-profile-error {
|
||||||
color: var(--colour-error);
|
color: var(--colour-error);
|
||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
|
|||||||
@@ -1,32 +1,90 @@
|
|||||||
|
import type { NumberFormat } from "@elysium/types";
|
||||||
|
|
||||||
|
// Named suffixes up to 1e33 (Decillion). Letter-based suffixes take over from 1e36 onwards.
|
||||||
|
const NAMED_SUFFIXES: { threshold: number; suffix: string }[] = [
|
||||||
|
{ threshold: 1e33, suffix: "Dc" }, // Decillion
|
||||||
|
{ threshold: 1e30, suffix: "No" }, // Nonillion
|
||||||
|
{ threshold: 1e27, suffix: "Oc" }, // Octillion
|
||||||
|
{ threshold: 1e24, suffix: "Sp" }, // Septillion
|
||||||
|
{ threshold: 1e21, suffix: "Sx" }, // Sextillion
|
||||||
|
{ threshold: 1e18, suffix: "Qi" }, // Quintillion
|
||||||
|
{ threshold: 1e15, suffix: "Qa" }, // Quadrillion
|
||||||
|
{ threshold: 1e12, suffix: "T" }, // Trillion
|
||||||
|
{ threshold: 1e9, suffix: "B" }, // Billion
|
||||||
|
{ threshold: 1e6, suffix: "M" }, // Million
|
||||||
|
{ threshold: 1e3, suffix: "K" }, // Thousand
|
||||||
|
];
|
||||||
|
|
||||||
|
// Letter suffixes start at 1e36 ("a"), stepping by 1000 each time (i.e. +3 exponent per letter).
|
||||||
|
const LETTER_BASE_EXP = 36;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formats a number with K/M/B/T/Q/Qt/S/Sp suffixes for display.
|
* Generates an alphabetic suffix for a given index:
|
||||||
* Numbers below 1000 show one decimal place.
|
* 0 → "a", 1 → "b", ..., 25 → "z",
|
||||||
|
* 26 → "aa", 27 → "ab", ..., 701 → "zz", 702 → "aaa", ...
|
||||||
*/
|
*/
|
||||||
export const formatNumber = (value: number): string => {
|
const getLetterSuffix = (index: number): string => {
|
||||||
if (!isFinite(value) || isNaN(value)) return "0";
|
let result = "";
|
||||||
if (value >= 1e24) {
|
let n = index;
|
||||||
return `${(value / 1e24).toFixed(2)}Sp`;
|
do {
|
||||||
|
result = String.fromCharCode(97 + (n % 26)) + result;
|
||||||
|
n = Math.floor(n / 26) - 1;
|
||||||
|
} while (n >= 0);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatSuffix = (value: number): string => {
|
||||||
|
if (value >= Math.pow(10, LETTER_BASE_EXP)) {
|
||||||
|
const exp = Math.floor(Math.log10(value));
|
||||||
|
const stepsAboveBase = Math.floor((exp - LETTER_BASE_EXP) / 3);
|
||||||
|
const divisorExp = LETTER_BASE_EXP + stepsAboveBase * 3;
|
||||||
|
const divisor = Math.pow(10, divisorExp);
|
||||||
|
return `${(value / divisor).toFixed(2)}${getLetterSuffix(stepsAboveBase)}`;
|
||||||
}
|
}
|
||||||
if (value >= 1e21) {
|
for (const { threshold, suffix } of NAMED_SUFFIXES) {
|
||||||
return `${(value / 1e21).toFixed(2)}S`;
|
if (value >= threshold) {
|
||||||
}
|
return `${(value / threshold).toFixed(2)}${suffix}`;
|
||||||
if (value >= 1e18) {
|
}
|
||||||
return `${(value / 1e18).toFixed(2)}Qt`;
|
|
||||||
}
|
|
||||||
if (value >= 1e15) {
|
|
||||||
return `${(value / 1e15).toFixed(2)}Q`;
|
|
||||||
}
|
|
||||||
if (value >= 1_000_000_000_000) {
|
|
||||||
return `${(value / 1_000_000_000_000).toFixed(2)}T`;
|
|
||||||
}
|
|
||||||
if (value >= 1_000_000_000) {
|
|
||||||
return `${(value / 1_000_000_000).toFixed(2)}B`;
|
|
||||||
}
|
|
||||||
if (value >= 1_000_000) {
|
|
||||||
return `${(value / 1_000_000).toFixed(2)}M`;
|
|
||||||
}
|
|
||||||
if (value >= 1_000) {
|
|
||||||
return `${(value / 1_000).toFixed(2)}K`;
|
|
||||||
}
|
}
|
||||||
return value.toFixed(1);
|
return value.toFixed(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a number in scientific notation: e.g. 1.23e15.
|
||||||
|
* Falls back to K/M/B/T style below 1 million.
|
||||||
|
*/
|
||||||
|
const formatScientific = (value: number): string => {
|
||||||
|
if (value < 1e6) return formatSuffix(value);
|
||||||
|
// toExponential handles all magnitudes JS can represent (up to ~1.8e308)
|
||||||
|
return value.toExponential(2).replace("e+", "e");
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a number in engineering notation (exponent always a multiple of 3):
|
||||||
|
* e.g. 12.35E12, 1.23E300. Falls back to K/M/B/T style below 1 million.
|
||||||
|
*/
|
||||||
|
const formatEngineering = (value: number): string => {
|
||||||
|
if (value < 1e6) return formatSuffix(value);
|
||||||
|
const exp = Math.floor(Math.log10(value));
|
||||||
|
const engExp = Math.floor(exp / 3) * 3;
|
||||||
|
const mantissa = value / Math.pow(10, engExp);
|
||||||
|
return `${mantissa.toFixed(2)}E${engExp}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a number for display using the player's chosen notation style.
|
||||||
|
* Negative values are formatted with a leading minus sign.
|
||||||
|
*/
|
||||||
|
export const formatNumber = (value: number, format: NumberFormat = "suffix"): string => {
|
||||||
|
if (!isFinite(value) || isNaN(value)) return "0";
|
||||||
|
if (value < 0) return `-${formatNumber(-value, format)}`;
|
||||||
|
|
||||||
|
switch (format) {
|
||||||
|
case "scientific":
|
||||||
|
return formatScientific(value);
|
||||||
|
case "engineering":
|
||||||
|
return formatEngineering(value);
|
||||||
|
default:
|
||||||
|
return formatSuffix(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -41,5 +41,5 @@ export type {
|
|||||||
UpgradeTarget,
|
UpgradeTarget,
|
||||||
} from "./interfaces/Upgrade.js";
|
} from "./interfaces/Upgrade.js";
|
||||||
export type { Zone, ZoneStatus } from "./interfaces/Zone.js";
|
export type { Zone, ZoneStatus } from "./interfaces/Zone.js";
|
||||||
export type { ProfileSettings } from "./interfaces/ProfileSettings.js";
|
export type { NumberFormat, ProfileSettings } from "./interfaces/ProfileSettings.js";
|
||||||
export { DEFAULT_PROFILE_SETTINGS } from "./interfaces/ProfileSettings.js";
|
export { DEFAULT_PROFILE_SETTINGS } from "./interfaces/ProfileSettings.js";
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
export type NumberFormat = "suffix" | "scientific" | "engineering";
|
||||||
|
|
||||||
export interface ProfileSettings {
|
export interface ProfileSettings {
|
||||||
showTotalGold: boolean;
|
showTotalGold: boolean;
|
||||||
showTotalClicks: boolean;
|
showTotalClicks: boolean;
|
||||||
@@ -7,6 +9,7 @@ export interface ProfileSettings {
|
|||||||
showQuestsCompleted: boolean;
|
showQuestsCompleted: boolean;
|
||||||
showAdventurersRecruited: boolean;
|
showAdventurersRecruited: boolean;
|
||||||
showAchievementsUnlocked: boolean;
|
showAchievementsUnlocked: boolean;
|
||||||
|
numberFormat: NumberFormat;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_PROFILE_SETTINGS: ProfileSettings = {
|
export const DEFAULT_PROFILE_SETTINGS: ProfileSettings = {
|
||||||
@@ -18,4 +21,5 @@ export const DEFAULT_PROFILE_SETTINGS: ProfileSettings = {
|
|||||||
showQuestsCompleted: true,
|
showQuestsCompleted: true,
|
||||||
showAdventurersRecruited: true,
|
showAdventurersRecruited: true,
|
||||||
showAchievementsUnlocked: true,
|
showAchievementsUnlocked: true,
|
||||||
|
numberFormat: "suffix",
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user