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:
@@ -1,4 +1,4 @@
|
||||
import type { Achievement, BossChallengeResponse, GameState } from "@elysium/types";
|
||||
import type { Achievement, BossChallengeResponse, GameState, NumberFormat } from "@elysium/types";
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
@@ -8,7 +8,8 @@ import {
|
||||
useState,
|
||||
} from "react";
|
||||
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 {
|
||||
@@ -54,6 +55,12 @@ interface GameContextValue {
|
||||
newAchievements: Achievement[];
|
||||
/** Remove an achievement from the toast queue */
|
||||
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);
|
||||
@@ -69,6 +76,7 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
|
||||
const [newAchievements, setNewAchievements] = useState<Achievement[]>([]);
|
||||
const [lastSavedAt, setLastSavedAt] = useState<number | null>(null);
|
||||
const [isSyncing, setIsSyncing] = useState(false);
|
||||
const [numberFormat, setNumberFormat] = useState<NumberFormat>("suffix");
|
||||
const stateRef = useRef<GameState | null>(null);
|
||||
const lastSaveRef = useRef<number>(Date.now());
|
||||
const isSyncingRef = useRef(false);
|
||||
@@ -87,6 +95,17 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
|
||||
if (data.offlineGold > 0) {
|
||||
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) {
|
||||
setError(err instanceof Error ? err.message : "Failed to load game");
|
||||
} finally {
|
||||
@@ -166,9 +185,10 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
|
||||
setState((prev) => {
|
||||
if (!prev) return prev;
|
||||
const clickPower = calculateClickPower(prev);
|
||||
const newGold = Math.min(prev.resources.gold + clickPower, RESOURCE_CAP);
|
||||
return {
|
||||
...prev,
|
||||
resources: { ...prev.resources, gold: prev.resources.gold + clickPower },
|
||||
resources: { ...prev.resources, gold: newGold },
|
||||
player: {
|
||||
...prev.player,
|
||||
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));
|
||||
}, []);
|
||||
|
||||
const boundFormatNumber = useCallback(
|
||||
(value: number) => formatNumberUtil(value, numberFormat),
|
||||
[numberFormat],
|
||||
);
|
||||
|
||||
return (
|
||||
<GameContext.Provider
|
||||
value={{
|
||||
@@ -431,6 +456,9 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
|
||||
dismissBattle,
|
||||
newAchievements,
|
||||
dismissAchievement,
|
||||
numberFormat,
|
||||
setNumberFormat,
|
||||
formatNumber: boundFormatNumber,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
Reference in New Issue
Block a user