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 { 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<CreateMessageOptions & InteractionCallbackOptions> {
@ -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,
@ -54,17 +55,24 @@ export async function formGuildOnlineMessage(guild_id: string): Promise<CreateMe
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<CreateMe
if (members.length == 0) {
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));
// group members by server
// Group members by server
const membersByServer = members.reduce((acc, member) => {
if (acc[member.server] == undefined) {
acc[member.server] = [];
@ -88,12 +101,29 @@ export async function formGuildOnlineMessage(guild_id: string): Promise<CreateMe
return acc;
}, {} as Record<string, typeof members>);
const output = Object.entries(membersByServer).map(([server, mx]) => {
return `**[${server}]** (${mx.length}): ${mx.map(m => m.name).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,
};
}
@ -101,38 +131,89 @@ export async function formGuildLeaderboardMessage(guild_id: string): Promise<Cre
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();
// 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 = [
md.heading("Guild Exp:"),
md.codeBlock(built),
].join("\n\n");
`# 📊 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,

View File

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

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