noot
Some checks failed
commit-tag / commit-tag-image (map[context:./migrations file:./migrations/Dockerfile name:migrations]) (push) Successful in 15s
commit-tag / commit-tag-image (map[context:./ts file:./ts/Dockerfile name:ts]) (push) Failing after 36s

This commit is contained in:
a 2025-06-14 16:55:31 -05:00
parent a8295b09d6
commit d5f3ea5848
No known key found for this signature in database
GPG Key ID: 2F22877AA4DFDADB
4 changed files with 168 additions and 52 deletions

1
ts/migrations Symbolic link
View File

@ -0,0 +1 @@
../migrations

View File

@ -3,6 +3,7 @@ import { PG } from "#/services/pg";
import { CreateMessageOptions, InteractionCallbackOptions } from "discordeno"; import { CreateMessageOptions, InteractionCallbackOptions } from "discordeno";
import { type } from "arktype"; import { type } from "arktype";
import { TabWriter } from "#/lib/util/tabwriter"; import { TabWriter } from "#/lib/util/tabwriter";
import { RANK_EMOJIS, getRankEmoji, formatNumber } from "#/lib/util/wynnfmt";
import * as md from 'ts-markdown-builder'; import * as md from 'ts-markdown-builder';
export async function formGuildInfoMessage(guild_id: string): Promise<CreateMessageOptions & InteractionCallbackOptions> { export async function formGuildInfoMessage(guild_id: string): Promise<CreateMessageOptions & InteractionCallbackOptions> {
@ -28,22 +29,22 @@ where ranked.uid = ${guild_id}
if (result.length == 0) { if (result.length == 0) {
return { return {
content: "no guild found", content: "No guild found.",
}; };
} }
const guild = result[0]; const guild = result[0];
const output = [ const output = [
md.heading("[wip] guild info"), `# 🏰 Guild Information`,
md.heading("overview"), `## **[${guild.prefix}] ${guild.name}**\n`,
`[${guild.prefix}] ${guild.name} `### 📊 Statistics`,
level: ${guild.level} `> **Level:** \`${guild.level}\``,
guild xp rank: ${guild.xp_rank >= 1000 ? "1000+" : guild.xp_rank} `> **Total XP:** \`${formatNumber(guild.xp)}\``,
xp: ${guild.xp} `> **XP Rank:** \`#${guild.xp_rank >= 1000 ? "1000+" : guild.xp_rank}\``,
territories: ${guild.territories} `> **Territories:** \`${guild.territories}\``,
wars: ${guild.wars}`, `> **Wars:** \`${guild.wars.toLocaleString()}\``,
].join("\n\n"); ].join("\n");
return { return {
content: output, content: output,
@ -54,17 +55,24 @@ export async function formGuildOnlineMessage(guild_id: string): Promise<CreateMe
const { sql } = await c.getAsync(PG); const { sql } = await c.getAsync(PG);
const result = await sql`select const result = await sql`select
name, gi.name as guild_name,
rank, gi.prefix as guild_prefix,
contributed, minecraft.user.name as name,
gm.rank,
gm.contributed,
minecraft.user.server as server minecraft.user.server as server
from wynn.guild_members inner join minecraft.user from wynn.guild_members gm
on minecraft.user.uid = wynn.guild_members.member_id 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 where minecraft.user.server is not null
and wynn.guild_members.guild_id = ${guild_id} and gm.guild_id = ${guild_id}
`; `;
const members = type({ const members = type({
guild_name: "string",
guild_prefix: "string",
name: "string", name: "string",
rank: "string", rank: "string",
contributed: "string", contributed: "string",
@ -73,13 +81,18 @@ export async function formGuildOnlineMessage(guild_id: string): Promise<CreateMe
if (members.length == 0) { if (members.length == 0) {
return { return {
content: "nobody is online :(", content: "😴 No guild members are currently online.",
}; };
} }
// Get guild info
const guildName = members[0].guild_name;
const guildPrefix = members[0].guild_prefix;
// Sort by contribution
members.sort((a, b) => Number(b.contributed) - Number(a.contributed)); members.sort((a, b) => Number(b.contributed) - Number(a.contributed));
// group members by server // Group members by server
const membersByServer = members.reduce((acc, member) => { const membersByServer = members.reduce((acc, member) => {
if (acc[member.server] == undefined) { if (acc[member.server] == undefined) {
acc[member.server] = []; acc[member.server] = [];
@ -88,12 +101,29 @@ export async function formGuildOnlineMessage(guild_id: string): Promise<CreateMe
return acc; return acc;
}, {} as Record<string, typeof members>); }, {} as Record<string, typeof members>);
const output = Object.entries(membersByServer).map(([server, mx]) => { // Sort servers by player count
return `**[${server}]** (${mx.length}): ${mx.map(m => m.name).join(", ")}`; 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(", "); }).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 { return {
content: `**total**: ${members.length} \n` + output, content: output,
}; };
} }
@ -101,38 +131,89 @@ export async function formGuildLeaderboardMessage(guild_id: string): Promise<Cre
const { sql } = await c.getAsync(PG); const { sql } = await c.getAsync(PG);
const result = await sql`select const result = await sql`select
name, gi.name as guild_name,
rank, gi.prefix as guild_prefix,
contributed minecraft.user.name as name,
from wynn.guild_members inner join minecraft.user gm.rank,
on minecraft.user.uid = wynn.guild_members.member_id gm.contributed
where wynn.guild_members.guild_id = ${guild_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 gm.guild_id = ${guild_id}
`; `;
const members = type({ const members = type({
guild_name: "string",
guild_prefix: "string",
name: "string", name: "string",
rank: "string", rank: "string",
contributed: "string", contributed: "string",
}).array().assert(result); }).array().assert(result);
const tw = new TabWriter(); if (members.length === 0) {
members.sort((a, b) => Number(b.contributed) - Number(a.contributed)); return {
let idx = 1; content: "No guild members found.",
for (const member of members.slice(0, 10)) { };
tw.add([
`${idx}.`,
member.rank,
member.name,
Number(member.contributed).toLocaleString(),
]);
idx = idx + 1;
} }
const built = tw.build(); // 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 = [ const output = [
md.heading("Guild Exp:"), `# 📊 Guild XP Leaderboard`,
md.codeBlock(built), `**[${guildPrefix}] ${guildName}**\n`,
].join("\n\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 { return {
content: output, content: output,

View File

@ -28,12 +28,11 @@ export class TabWriter {
return ""; return "";
} }
const columnWidths = this.columns.map(col => col.reduce((a, b) => Math.max(a, b.length+this.spacing), 0)); 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++) { for(let i = 0; i < this.columns[0].length; i++) {
if (i > 0) out += "\n";
for(let j = 0; j < this.columns.length; j++) { for(let j = 0; j < this.columns.length; j++) {
out+= this.columns[j][i].padEnd(columnWidths[j]); out+= this.columns[j][i].padEnd(columnWidths[j]);
} }
out+= "\n";
} }
return out; return out;
} }

View File

@ -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();
}