feat: Implement a net worth leaderboard by aggregating user balance and inventory item values.

This commit is contained in:
syntaxbullet
2026-01-05 16:40:26 +01:00
parent a2596d4124
commit 9a32ab298d
2 changed files with 59 additions and 17 deletions

View File

@@ -1,8 +1,8 @@
import { createCommand } from "@/lib/utils"; import { createCommand } from "@/lib/utils";
import { SlashCommandBuilder } from "discord.js"; import { SlashCommandBuilder } from "discord.js";
import { DrizzleClient } from "@/lib/DrizzleClient"; import { DrizzleClient } from "@/lib/DrizzleClient";
import { users } from "@/db/schema"; import { users, items, inventory } from "@/db/schema";
import { desc } from "drizzle-orm"; import { desc, sql, eq } from "drizzle-orm";
import { createWarningEmbed } from "@lib/embeds"; import { createWarningEmbed } from "@lib/embeds";
import { getLeaderboardEmbed } from "@/modules/leveling/leveling.view"; import { getLeaderboardEmbed } from "@/modules/leveling/leveling.view";
@@ -12,30 +12,49 @@ export const leaderboard = createCommand({
.setDescription("View the top players") .setDescription("View the top players")
.addStringOption(option => .addStringOption(option =>
option.setName("type") option.setName("type")
.setDescription("Sort by XP or Balance") .setDescription("Sort by XP, Balance, or Net Worth")
.setRequired(true) .setRequired(true)
.addChoices( .addChoices(
{ name: "Level / XP", value: "xp" }, { name: "Level / XP", value: "xp" },
{ name: "Balance", value: "balance" } { name: "Balance", value: "balance" },
{ name: "Net Worth", value: "networth" }
) )
), ),
execute: async (interaction) => { execute: async (interaction) => {
await interaction.deferReply(); await interaction.deferReply();
const type = interaction.options.getString("type", true); const type = interaction.options.getString("type", true);
const isXp = type === "xp";
const leaders = await DrizzleClient.query.users.findMany({ let leaders;
orderBy: isXp ? desc(users.xp) : desc(users.balance),
limit: 10 if (type === 'networth') {
}); leaders = await DrizzleClient.select({
username: users.username,
level: users.level,
xp: users.xp,
balance: users.balance,
netWorth: sql<bigint>`${users.balance} + COALESCE(SUM(${items.price} * ${inventory.quantity}), 0)`.as('net_worth')
})
.from(users)
.leftJoin(inventory, eq(users.id, inventory.userId))
.leftJoin(items, eq(inventory.itemId, items.id))
.groupBy(users.id)
.orderBy(desc(sql`net_worth`))
.limit(10);
} else {
const isXp = type === "xp";
leaders = await DrizzleClient.query.users.findMany({
orderBy: isXp ? desc(users.xp) : desc(users.balance),
limit: 10
});
}
if (leaders.length === 0) { if (leaders.length === 0) {
await interaction.editReply({ embeds: [createWarningEmbed("No users found.", "Leaderboard")] }); await interaction.editReply({ embeds: [createWarningEmbed("No users found.", "Leaderboard")] });
return; return;
} }
const embed = getLeaderboardEmbed(leaders, isXp ? 'xp' : 'balance'); const embed = getLeaderboardEmbed(leaders, type as 'xp' | 'balance' | 'networth');
await interaction.editReply({ embeds: [embed] }); await interaction.editReply({ embeds: [embed] });
} }

View File

@@ -8,6 +8,7 @@ interface LeaderboardUser {
level: number | null; level: number | null;
xp: bigint | null; xp: bigint | null;
balance: bigint | null; balance: bigint | null;
netWorth?: bigint | null;
} }
/** /**
@@ -23,23 +24,45 @@ function getMedalEmoji(index: number): string {
/** /**
* Formats a single leaderboard entry based on type * Formats a single leaderboard entry based on type
*/ */
function formatLeaderEntry(user: LeaderboardUser, index: number, type: 'xp' | 'balance'): string { function formatLeaderEntry(user: LeaderboardUser, index: number, type: 'xp' | 'balance' | 'networth'): string {
const medal = getMedalEmoji(index); const medal = getMedalEmoji(index);
const value = type === 'xp' let value = '';
? `Lvl ${user.level ?? 1} (${user.xp ?? 0n} XP)`
: `${user.balance ?? 0n} 🪙`; switch (type) {
case 'xp':
value = `Lvl ${user.level ?? 1} (${user.xp ?? 0n} XP)`;
break;
case 'balance':
value = `${user.balance ?? 0n} 🪙`;
break;
case 'networth':
value = `${user.netWorth ?? 0n} 🪙 (Net Worth)`;
break;
}
return `${medal} **${user.username}** — ${value}`; return `${medal} **${user.username}** — ${value}`;
} }
/** /**
* Creates a leaderboard embed for either XP or Balance rankings * Creates a leaderboard embed for either XP, Balance or Net Worth rankings
*/ */
export function getLeaderboardEmbed(leaders: LeaderboardUser[], type: 'xp' | 'balance'): EmbedBuilder { export function getLeaderboardEmbed(leaders: LeaderboardUser[], type: 'xp' | 'balance' | 'networth'): EmbedBuilder {
const description = leaders.map((user, index) => const description = leaders.map((user, index) =>
formatLeaderEntry(user, index, type) formatLeaderEntry(user, index, type)
).join("\n"); ).join("\n");
const title = type === 'xp' ? "🏆 XP Leaderboard" : "💰 Richest Players"; let title = '';
switch (type) {
case 'xp':
title = "🏆 XP Leaderboard";
break;
case 'balance':
title = "💰 Richest Players";
break;
case 'networth':
title = "💎 Net Worth Leaderboard";
break;
}
return new EmbedBuilder() return new EmbedBuilder()
.setTitle(title) .setTitle(title)