diff --git a/devplacepy/static/css/game.css b/devplacepy/static/css/game.css
index da52bded..3e921310 100644
--- a/devplacepy/static/css/game.css
+++ b/devplacepy/static/css/game.css
@@ -250,7 +250,7 @@
flex-direction: column;
align-items: flex-start;
flex: 1;
- min-width: 0;
+ min-width: 80px;
overflow: hidden;
}
@@ -262,6 +262,7 @@
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
+ min-width: 80px;
max-width: 100%;
}
@@ -269,23 +270,24 @@
color: var(--accent);
}
+.game-lb-title {
+ display: inline-block;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ max-width: 100px;
+ color: var(--accent);
+ font-size: 0.75rem;
+ font-style: italic;
+ vertical-align: baseline;
+}
+
.game-lb-level {
flex-shrink: 0;
color: var(--text-secondary);
font-size: 0.8rem;
}
-.game-lb-title {
- color: var(--accent);
- font-size: 0.7rem;
- font-style: italic;
- display: block;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- max-width: 100%;
-}
-
.game-lb-score {
flex-shrink: 0;
color: var(--text-muted);
@@ -570,3 +572,4 @@
font-size: 0.75rem;
font-weight: 600;
}
+
diff --git a/devplacepy/static/js/GameFarm.js b/devplacepy/static/js/GameFarm.js
index 4c8d1997..59670d4b 100644
--- a/devplacepy/static/js/GameFarm.js
+++ b/devplacepy/static/js/GameFarm.js
@@ -386,9 +386,9 @@ export class GameFarm {
const board = this.board || "score";
list.innerHTML = data.entries
.map((entry) => {
- const title = entry.title ? `${entry.title}` : "";
+ const titleHtml = entry.title ? ` ${entry.title}` : "";
const value = this._leaderboardValue(board, entry);
- return `
#${entry.rank}Lv ${entry.level}${value}`;
+ return `#${entry.rank}${entry.username}${titleHtml}Lv ${entry.level}${value}`;
})
.join("");
} catch (error) {
@@ -402,3 +402,4 @@ export class GameFarm {
return Format.exact(entry.score);
}
}
+
diff --git a/tests/api/game/index.py b/tests/api/game/index.py
index adc3cf0f..02b9b06e 100644
--- a/tests/api/game/index.py
+++ b/tests/api/game/index.py
@@ -111,6 +111,50 @@ def test_leaderboard_entries_carry_score_and_prestige(app_server, seeded_db):
assert mine["score"] >= 50 * economy.SCORE_PRESTIGE
+def test_leaderboard_every_entry_has_username_and_title(app_server, seeded_db):
+ session, name = _signup()
+ _reset_farm(name)
+ _set_farm(name, prestige=1)
+ deadline = time.time() + 20
+ while True:
+ response = requests.get(f"{BASE_URL}/game/leaderboard", headers=JSON)
+ assert response.status_code == 200
+ entries = response.json()["entries"]
+ if any(e["username"] == name for e in entries) or time.time() >= deadline:
+ break
+ time.sleep(0.5)
+ assert entries
+ for entry in entries:
+ assert isinstance(entry["username"], str) and len(entry["username"]) > 0
+ assert isinstance(entry["title"], str)
+
+
+def test_leaderboard_username_differs_from_title(app_server, seeded_db):
+ session, name = _signup()
+ _reset_farm(name)
+ _set_farm(name, prestige=1)
+ from devplacepy.services.game.store.cosmetics import equip_title
+ from devplacepy.database import get_table
+ user = get_table("users").find_one(username=name)
+ equip_title(user, "title_refactorer")
+ from devplacepy.services.game.store.farm import _leaderboard_cache, _board_cache
+ _leaderboard_cache.clear()
+ _board_cache.clear()
+ deadline = time.time() + 20
+ while True:
+ response = requests.get(f"{BASE_URL}/game/leaderboard", headers=JSON)
+ assert response.status_code == 200
+ entries = response.json()["entries"]
+ mine = next((e for e in entries if e["username"] == name), None)
+ if mine is not None or time.time() >= deadline:
+ break
+ time.sleep(0.5)
+ assert mine is not None
+ assert mine["title"] == "Serial Refactorer"
+ assert mine["username"] == name
+ assert mine["username"] != mine["title"]
+
+
def test_plant_returns_updated_farm(app_server, seeded_db):
session, name = _signup()
_reset_farm(name)
@@ -186,3 +230,4 @@ def test_prestige_below_level_returns_400(app_server, seeded_db):
_reset_farm(name)
response = session.post(f"{BASE_URL}/game/prestige", headers=JSON)
assert response.status_code == 400
+
diff --git a/tests/e2e/game/index.py b/tests/e2e/game/index.py
index 695372b7..ea7de47a 100644
--- a/tests/e2e/game/index.py
+++ b/tests/e2e/game/index.py
@@ -365,6 +365,29 @@ def test_cosmetics_buy_and_equip_flow(alice):
expect(page.locator(".game-cosmetics")).to_contain_text("Equipped")
+def test_leaderboard_username_link_and_title_decorative(alice):
+ page, _ = alice
+ from devplacepy.database import get_table
+ from devplacepy.services.game import store
+ from devplacepy.services.game.store.cosmetics import equip_title
+ from devplacepy.services.game.store.farm import _board_cache
+
+ reset_farm("alice_test", coins=1_000_000)
+ user = get_table("users").find_one(username="alice_test")
+ store.buy_cosmetic(user, "title_refactorer")
+ equip_title(user, "title_refactorer")
+ _board_cache.clear()
+ open_game(page)
+ page.locator(".game-lb-row").first.wait_for(state="visible")
+ name_link = page.locator("a.game-lb-name:has-text('alice_test')")
+ expect(name_link).to_be_visible()
+ href = name_link.get_attribute("href")
+ assert href and "alice_test" in href
+ title = page.locator(".game-lb-title")
+ expect(title).to_be_visible()
+ assert title.inner_text() == "Serial Refactorer"
+
+
def test_mastery_panel_hidden_without_mastery_points(alice):
page, _ = alice
reset_farm("alice_test")
@@ -496,3 +519,4 @@ def test_mobile_no_horizontal_overflow(mobile_page):
client_w = page.evaluate("document.documentElement.clientWidth")
assert scroll_w - client_w <= 1, f"horizontal overflow: {scroll_w} vs {client_w}"
assert page.locator("[data-game-grid]").first.is_visible()
+
diff --git a/tests/unit/services/game/store.py b/tests/unit/services/game/store.py
index e69b41b8..765e3cec 100644
--- a/tests/unit/services/game/store.py
+++ b/tests/unit/services/game/store.py
@@ -755,7 +755,28 @@ def test_leaderboard_entry_exposes_score_and_prestige(local_db):
farm = store.get_farm(user["uid"])
assert entry["prestige"] == 3
assert entry["score"] == economy.farm_score(farm)
- assert {"rank", "username", "level", "xp", "coins", "total_harvests", "prestige", "score"} <= set(entry)
+ assert {"rank", "username", "level", "xp", "coins", "total_harvests", "prestige", "score", "title"} <= set(entry)
+
+
+def test_leaderboard_entry_title_empty_when_no_title_equipped(local_db):
+ user = _reset("unit_lb_no_title")
+ _set(user, prestige=1)
+ entry = next(e for e in store.leaderboard(50) if e["username"] == "unit_lb_no_title")
+ assert "title" in entry
+ assert entry["title"] == ""
+
+
+def test_leaderboard_entry_title_is_display_name_when_equipped(local_db):
+ user = _reset("unit_lb_title_user", coins=300_000)
+ _set(user, prestige=1)
+ from devplacepy.services.game.store.cosmetics import equip_title
+ store.buy_cosmetic(user, "title_refactorer")
+ equip_title(user, "title_refactorer")
+ _clear_game_caches()
+ entry = next(e for e in store.leaderboard(50) if e["username"] == "unit_lb_title_user")
+ assert entry["title"] == "Serial Refactorer"
+ assert entry["username"] == "unit_lb_title_user"
+ assert entry["title"] != entry["username"]
# --- market saturation ---------------------------------------------------------
@@ -1177,3 +1198,6 @@ def test_claim_grant_partial_treasury(local_db):
result = store.claim_grant(user)
assert result["amount"] == 300
assert store.treasury_balance() == 0
+
+
+