From d5f3ea58481b2da29cb5ab8425645f7d7b20b773 Mon Sep 17 00:00:00 2001 From: a Date: Sat, 14 Jun 2025 16:55:31 -0500 Subject: [PATCH] noot --- ts/migrations | 1 + ts/src/activities/guild_messages.ts | 181 ++++++++++++++++++++-------- ts/src/lib/util/tabwriter.ts | 3 +- ts/src/lib/util/wynnfmt.ts | 35 ++++++ 4 files changed, 168 insertions(+), 52 deletions(-) create mode 120000 ts/migrations create mode 100644 ts/src/lib/util/wynnfmt.ts diff --git a/ts/migrations b/ts/migrations new file mode 120000 index 0000000..f0dcf84 --- /dev/null +++ b/ts/migrations @@ -0,0 +1 @@ +../migrations \ No newline at end of file diff --git a/ts/src/activities/guild_messages.ts b/ts/src/activities/guild_messages.ts index a0f7a54..8ddc8f3 100644 --- a/ts/src/activities/guild_messages.ts +++ b/ts/src/activities/guild_messages.ts @@ -3,11 +3,12 @@ import { PG } from "#/services/pg"; import { CreateMessageOptions, InteractionCallbackOptions } from "discordeno"; import { type } from "arktype"; import { TabWriter } from "#/lib/util/tabwriter"; +import { RANK_EMOJIS, getRankEmoji, formatNumber } from "#/lib/util/wynnfmt"; import * as md from 'ts-markdown-builder'; export async function formGuildInfoMessage(guild_id: string): Promise { const { sql } = await c.getAsync(PG); - + const result = await sql` with ranked as ( select @@ -28,22 +29,22 @@ where ranked.uid = ${guild_id} if (result.length == 0) { return { - content: "no guild found", + content: "No guild found.", }; } const guild = result[0]; const output = [ - md.heading("[wip] guild info"), - md.heading("overview"), - `[${guild.prefix}] ${guild.name} -level: ${guild.level} -guild xp rank: ${guild.xp_rank >= 1000 ? "1000+" : guild.xp_rank} -xp: ${guild.xp} -territories: ${guild.territories} -wars: ${guild.wars}`, - ].join("\n\n"); + `# 🏰 Guild Information`, + `## **[${guild.prefix}] ${guild.name}**\n`, + `### 📊 Statistics`, + `> **Level:** \`${guild.level}\``, + `> **Total XP:** \`${formatNumber(guild.xp)}\``, + `> **XP Rank:** \`#${guild.xp_rank >= 1000 ? "1000+" : guild.xp_rank}\``, + `> **Territories:** \`${guild.territories}\``, + `> **Wars:** \`${guild.wars.toLocaleString()}\``, + ].join("\n"); return { content: output, @@ -52,19 +53,26 @@ wars: ${guild.wars}`, export async function formGuildOnlineMessage(guild_id: string): Promise { const { sql } = await c.getAsync(PG); - + const result = await sql`select - name, - rank, - contributed, + gi.name as guild_name, + gi.prefix as guild_prefix, + minecraft.user.name as name, + gm.rank, + gm.contributed, minecraft.user.server as server - from wynn.guild_members inner join minecraft.user - on minecraft.user.uid = wynn.guild_members.member_id + from wynn.guild_members gm + inner join minecraft.user + on minecraft.user.uid = gm.member_id + inner join wynn.guild_info gi + on gi.uid = gm.guild_id where minecraft.user.server is not null - and wynn.guild_members.guild_id = ${guild_id} + and gm.guild_id = ${guild_id} `; - + const members = type({ + guild_name: "string", + guild_prefix: "string", name: "string", rank: "string", contributed: "string", @@ -73,13 +81,18 @@ export async function formGuildOnlineMessage(guild_id: string): Promise Number(b.contributed) - Number(a.contributed)); - // group members by server + // Group members by server const membersByServer = members.reduce((acc, member) => { if (acc[member.server] == undefined) { acc[member.server] = []; @@ -88,53 +101,121 @@ export async function formGuildOnlineMessage(guild_id: string): Promise); - const output = Object.entries(membersByServer).map(([server, mx]) => { - return `**[${server}]** (${mx.length}): ${mx.map(m => m.name).join(", ")}`; - }).join(", "); + // Sort servers by player count + const sortedServers = Object.entries(membersByServer) + .sort(([, a], [, b]) => b.length - a.length); + + // Build server sections + const serverSections = sortedServers.map(([server, serverMembers]) => { + const memberList = serverMembers.map(m => { + const emoji = getRankEmoji(m.rank); + return `${emoji} ${m.name}`; + }).join(", "); + + return `### 🌐 ${server} (${serverMembers.length} player${serverMembers.length > 1 ? 's' : ''})\n> ${memberList}`; + }); + + const output = [ + `# 🟢 Online Guild Members`, + `**[${guildPrefix}] ${guildName}**\n`, + `📊 **Total Online:** \`${members.length}\` members across \`${sortedServers.length}\` servers\n`, + ...serverSections + ].join("\n"); return { - content: `**total**: ${members.length} \n` + output, + content: output, }; } export async function formGuildLeaderboardMessage(guild_id: string): Promise { const { sql } = await c.getAsync(PG); - + const result = await sql`select - name, - rank, - contributed - from wynn.guild_members inner join minecraft.user - on minecraft.user.uid = wynn.guild_members.member_id - where wynn.guild_members.guild_id = ${guild_id} + gi.name as guild_name, + gi.prefix as guild_prefix, + minecraft.user.name as name, + gm.rank, + gm.contributed + from wynn.guild_members gm + inner join minecraft.user + on minecraft.user.uid = gm.member_id + inner join wynn.guild_info gi + on gi.uid = gm.guild_id + where gm.guild_id = ${guild_id} `; - + const members = type({ + guild_name: "string", + guild_prefix: "string", name: "string", rank: "string", contributed: "string", }).array().assert(result); - const tw = new TabWriter(); - members.sort((a, b) => Number(b.contributed) - Number(a.contributed)); - let idx = 1; - for (const member of members.slice(0, 10)) { - tw.add([ - `${idx}.`, - member.rank, - member.name, - Number(member.contributed).toLocaleString(), - ]); - idx = idx + 1; + if (members.length === 0) { + return { + content: "No guild members found.", + }; } - const built = tw.build(); - const output = [ - md.heading("Guild Exp:"), - md.codeBlock(built), - ].join("\n\n"); + // Sort by contribution + members.sort((a, b) => Number(b.contributed) - Number(a.contributed)); + const topMembers = members.slice(0, 15); + + // Get guild info from first member (all have same guild info) + const guildName = members[0].guild_name; + const guildPrefix = members[0].guild_prefix; + + // Calculate total guild XP + const totalXP = members.reduce((sum, m) => sum + Number(m.contributed), 0); + + // Build the leaderboard with proper alignment + const tw = new TabWriter(2); + // Add header row + tw.add(["#", "Rank", "Player", "XP", "%"]); + tw.add(["───", "────────────", "────────────────", "──────────", "──────"]); // Separator line + + topMembers.forEach((member, index) => { + const position = index + 1; + const posStr = position === 1 ? "🥇" : position === 2 ? "🥈" : position === 3 ? "🥉" : `${position}.`; + const rankEmoji = getRankEmoji(member.rank); + const contribution = Number(member.contributed); + const percentage = ((contribution / totalXP) * 100).toFixed(1); + + // Use formatNumber for consistent formatting + const contribFormatted = contribution >= 10_000 + ? formatNumber(contribution) + : contribution.toLocaleString(); + + tw.add([ + posStr, + `${rankEmoji} ${member.rank}`, + member.name, + contribFormatted, + `${percentage}%` + ]); + }); + + const leaderboardTable = tw.build(); + + // Create summary stats + const avgContribution = Math.floor(totalXP / members.length); + + const output = [ + `# 📊 Guild XP Leaderboard`, + `**[${guildPrefix}] ${guildName}**\n`, + `📈 **Total Guild XP:** \`${totalXP.toLocaleString()}\``, + `👥 **Total Members:** \`${members.length}\``, + `📊 **Average Contribution:** \`${avgContribution.toLocaleString()}\`\n`, + `### Top Contributors`, + "```", + leaderboardTable, + "```", + `*Showing top 15 of ${members.length} members*` + ].join("\n"); + return { content: output, }; -} \ No newline at end of file +} diff --git a/ts/src/lib/util/tabwriter.ts b/ts/src/lib/util/tabwriter.ts index 11e2747..597aa37 100644 --- a/ts/src/lib/util/tabwriter.ts +++ b/ts/src/lib/util/tabwriter.ts @@ -28,12 +28,11 @@ export class TabWriter { return ""; } const columnWidths = this.columns.map(col => col.reduce((a, b) => Math.max(a, b.length+this.spacing), 0)); - console.log(columnWidths) for(let i = 0; i < this.columns[0].length; i++) { + if (i > 0) out += "\n"; for(let j = 0; j < this.columns.length; j++) { out+= this.columns[j][i].padEnd(columnWidths[j]); } - out+= "\n"; } return out; } diff --git a/ts/src/lib/util/wynnfmt.ts b/ts/src/lib/util/wynnfmt.ts new file mode 100644 index 0000000..ea769fc --- /dev/null +++ b/ts/src/lib/util/wynnfmt.ts @@ -0,0 +1,35 @@ +/** + * Wynncraft formatting utilities + */ + +/** + * Mapping of Wynncraft guild ranks to their corresponding emojis + */ +export const RANK_EMOJIS = { + "OWNER": "👑", + "CHIEF": "⭐", + "STRATEGIST": "🎯", + "CAPTAIN": "⚔️", + "RECRUITER": "📢", + "RECRUIT": "🌱", +} as const; + +/** + * Get the emoji for a given guild rank + * @param rank - The guild rank name + * @returns The corresponding emoji or a default bullet point + */ +export function getRankEmoji(rank: string): string { + return RANK_EMOJIS[rank as keyof typeof RANK_EMOJIS] || "•"; +} + +/** + * Format large numbers with K/M suffixes + * @param num - The number to format + * @returns Formatted string with appropriate suffix + */ +export function formatNumber(num: number): string { + if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(1)}M`; + if (num >= 1_000) return `${(num / 1_000).toFixed(0)}K`; + return num.toLocaleString(); +} \ No newline at end of file