feat(client): configurable leaderboard and team stats columns (#4646)

## Description:

- rips out some duped code, making tables use the same layout, where the
only thing different is the row content.
- updated text to be emojis to save space
- updated columns to be as wide as they need to be
- updated cells to have left/right thin borders to read easier
- added more supported column types
- removed the + button to always show everyone, where the current player
is either at the bottom or in the top 5
- added a cogwheel to change options
- can select different options for each menu type (player/team stats)

<img width="582" height="630" alt="image"
src="https://github.com/user-attachments/assets/e54dc3d5-eb2c-4bab-8732-06bea68aec45"
/>

works fine on mobile, will probably need some future love though (has
h-scroll, so all options can be seen by scrolling):


## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory

## Please put your Discord username so you can be contacted if a bug or
regression is found:

w.o.n
This commit is contained in:
Ryan
2026-07-22 13:13:43 -07:00
committed by GitHub
parent 3e2251bbbb
commit d76a0c4009
25 changed files with 1697 additions and 626 deletions
+78 -1
View File
@@ -1,4 +1,9 @@
import { EFFECTS_KEY, UserSettings } from "../src/core/game/UserSettings";
import {
EFFECTS_KEY,
PLAYER_STATS_COLUMNS_KEY,
TEAM_STATS_COLUMNS_KEY,
UserSettings,
} from "../src/core/game/UserSettings";
describe("UserSettings effect selection", () => {
beforeEach(() => {
@@ -58,3 +63,75 @@ describe("UserSettings effect selection", () => {
expect(s.getSelectedEffectName("hydro")).toBe("hydro_boom");
});
});
describe("UserSettings stats columns", () => {
beforeEach(() => {
localStorage.clear();
(
UserSettings as unknown as { cache: Map<string, string | null> }
).cache.clear();
});
it("returns defaults when nothing is stored", () => {
expect(new UserSettings().statsColumns("player")).toEqual([
"tiles",
"gold",
"maxtroops",
]);
expect(new UserSettings().statsColumns("team")).toEqual([
"tiles",
"gold",
"maxtroops",
]);
});
it("round-trips a selection in registry order", () => {
const s = new UserSettings();
// Stored order is check order; getter returns registry (display) order.
s.setStatsColumns("player", ["warships", "gold"]);
expect(s.statsColumns("player")).toEqual(["gold", "warships"]);
});
it("filters unknown ids", () => {
localStorage.setItem(
PLAYER_STATS_COLUMNS_KEY,
JSON.stringify(["gold", "bogus"]),
);
expect(new UserSettings().statsColumns("player")).toEqual(["gold"]);
});
it("drops the removed attacks column from persisted selections", () => {
localStorage.setItem(
PLAYER_STATS_COLUMNS_KEY,
JSON.stringify(["attacks", "gold"]),
);
expect(new UserSettings().statsColumns("player")).toEqual(["gold"]);
});
it("falls back to defaults on corrupt JSON", () => {
localStorage.setItem(PLAYER_STATS_COLUMNS_KEY, "not json");
expect(new UserSettings().statsColumns("player")).toEqual([
"tiles",
"gold",
"maxtroops",
]);
});
it("falls back to defaults when no valid ids remain", () => {
localStorage.setItem(PLAYER_STATS_COLUMNS_KEY, JSON.stringify(["bogus"]));
expect(new UserSettings().statsColumns("player")).toEqual([
"tiles",
"gold",
"maxtroops",
]);
});
it("keeps player and team selections independent", () => {
const s = new UserSettings();
s.setStatsColumns("player", ["gold"]);
s.setStatsColumns("team", ["warships"]);
expect(s.statsColumns("player")).toEqual(["gold"]);
expect(s.statsColumns("team")).toEqual(["warships"]);
expect(localStorage.getItem(TEAM_STATS_COLUMNS_KEY)).toBe('["warships"]');
});
});