From efc77a4c35ca5f01e9d13be75aefaf06753ab655 Mon Sep 17 00:00:00 2001 From: holzmaster Date: Thu, 4 Dec 2025 15:21:27 +0100 Subject: [PATCH 1/4] Add vibe-coded stats --- src/service/stats.ts | 402 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 402 insertions(+) create mode 100644 src/service/stats.ts diff --git a/src/service/stats.ts b/src/service/stats.ts new file mode 100644 index 00000000..51a6c0b2 --- /dev/null +++ b/src/service/stats.ts @@ -0,0 +1,402 @@ +// +// !CAUTION! this file is entirely vibe-coded and only used for dönerjesus-wrapped +// !CAUTION! It will be deleted in 2026-01-01 +// + +import { sql } from "kysely"; +import type { Snowflake } from "discord.js"; + +import type { Database } from "#storage/db/model.ts"; +import type { Kysely } from "kysely"; +import db from "#db"; + +/** + * Statistics about polls + */ +export interface PollStats { + totalPolls: number; + totalVotes: number; + userPolls?: number; + userVotes?: number; +} + +/** + * Statistics about user inventory + */ +export interface InventoryStats { + itemCount: number; + percentile: number; +} + +/** + * Statistics about honor points + */ +export interface HonorStats { + collectedPoints: number; + awardedPoints: number; + votesGiven: number; +} + +/** + * Statistics about penis measurements + */ +export interface PenisStats { + averageSize: number; + averageRadius: number; + minSize: number; + maxSize: number; + minRadius: number; + maxRadius: number; + userSize?: number; + userRadius?: number; + userSizePercentile?: number; + userRadiusPercentile?: number; +} + +/** + * Statistics about boobs measurements + */ +export interface BoobsStats { + averageSize: number; + minSize: number; + maxSize: number; + userSize?: number; + userSizePercentile?: number; +} + +/** + * Statistics about emotes + */ +export interface EmoteStats { + emoteId: Snowflake; + emoteName: string; + isAnimated: boolean; + usageCount: number; +} + +/** + * Get poll statistics + */ +export async function getPollStats( + userId?: Snowflake, + ctx: Kysely = db(), +): Promise { + const totalPollsResult = await ctx + .selectFrom("polls") + .select(({ fn }) => fn.countAll().as("count")) + .executeTakeFirstOrThrow(); + + const totalVotesResult = await ctx + .selectFrom("pollAnswers") + .select(({ fn }) => fn.countAll().as("count")) + .executeTakeFirstOrThrow(); + + const result: PollStats = { + totalPolls: totalPollsResult.count, + totalVotes: totalVotesResult.count, + }; + + if (userId) { + const userPollsResult = await ctx + .selectFrom("polls") + .where("authorId", "=", userId) + .select(({ fn }) => fn.countAll().as("count")) + .executeTakeFirstOrThrow(); + + const userVotesResult = await ctx + .selectFrom("pollAnswers") + .where("userId", "=", userId) + .select(({ fn }) => fn.countAll().as("count")) + .executeTakeFirstOrThrow(); + + result.userPolls = userPollsResult.count; + result.userVotes = userVotesResult.count; + } + + return result; +} + +/** + * Get inventory statistics for a user + */ +export async function getInventoryStats( + userId: Snowflake, + ctx: Kysely = db(), +): Promise { + // Get user's item count (excluding deleted items) + const userItemsResult = await ctx + .selectFrom("loot") + .where("winnerId", "=", userId) + .where("deletedAt", "is", null) + .select(({ fn }) => fn.countAll().as("count")) + .executeTakeFirstOrThrow(); + + const itemCount = userItemsResult.count; + + // Get all users' item counts for percentile calculation + const allUserCounts = await ctx + .selectFrom("loot") + .where("deletedAt", "is", null) + .groupBy("winnerId") + .select(["winnerId", ({ fn }) => fn.countAll().as("count")]) + .orderBy("count", "asc") + .execute(); + + if (allUserCounts.length === 0) { + return { + itemCount, + percentile: 0, + }; + } + + // Calculate percentile + const totalUsers = allUserCounts.length; + const usersWithLessItems = allUserCounts.filter(u => u.count < itemCount).length; + const percentile = (usersWithLessItems / totalUsers) * 100; + + return { + itemCount, + percentile: Math.round(percentile * 100) / 100, // Round to 2 decimal places + }; +} + +/** + * Get honor points statistics for a user + */ +export async function getHonorStats( + userId: Snowflake, + ctx: Kysely = db(), +): Promise { + // Get collected points + const collectedPointsResult = await ctx + .selectFrom("ehrePoints") + .where("userId", "=", userId) + .select("points") + .executeTakeFirst(); + + const collectedPoints = collectedPointsResult?.points ?? 0; + + // Get votes given (awards given to others) + const votesGivenResult = await ctx + .selectFrom("ehreVotes") + .where("userId", "=", userId) + .select(({ fn }) => fn.countAll().as("count")) + .executeTakeFirstOrThrow(); + + const votesGiven = votesGivenResult.count; + + // Calculate awarded points by summing up points given to others + // This requires looking at who received points from this user's votes + // We need to calculate the vote values for each vote given + // For simplicity, we'll count votes given and estimate points awarded + // The actual calculation would require the same logic as in ehre.ts getVoteValue + // For now, we'll just return the vote count as a proxy + + // To get actual awarded points, we'd need to track this separately or calculate it + // For now, let's approximate by counting votes given + // In reality, awarded points depend on the voter's position in the honor ranking + // which changes over time, so exact calculation would be complex + const awardedPoints = votesGiven; // Approximate as 1 point per vote (minimum) + + return { + collectedPoints, + awardedPoints, + votesGiven, + }; +} + +/** + * Get penis statistics + */ +export async function getPenisStats( + userId?: Snowflake, + ctx: Kysely = db(), +): Promise { + // Get latest measurement for each user using a join with max id subquery + // We use id as a proxy for "latest" since it's auto-incrementing + const maxIdsSubquery = ctx + .selectFrom("penis") + .select(["userId", ({ fn }) => fn.max("id").as("maxId")]) + .groupBy("userId") + .as("maxIds"); + + const latestMeasurements = await ctx + .selectFrom("penis") + .innerJoin(maxIdsSubquery, join => + join.onRef("penis.userId", "=", "maxIds.userId").onRef("penis.id", "=", "maxIds.maxId"), + ) + .select(["penis.userId", "penis.size", "penis.radius"]) + .execute(); + + // Calculate statistics from latest measurements + if (latestMeasurements.length === 0) { + return { + averageSize: 0, + averageRadius: 0, + minSize: 0, + maxSize: 0, + minRadius: 0, + maxRadius: 0, + }; + } + + const sizes = latestMeasurements.map(m => m.size); + const radii = latestMeasurements.map(m => m.radius); + + const averageSize = sizes.reduce((a, b) => a + b, 0) / sizes.length; + const averageRadius = radii.reduce((a, b) => a + b, 0) / radii.length; + const minSize = Math.min(...sizes); + const maxSize = Math.max(...sizes); + const minRadius = Math.min(...radii); + const maxRadius = Math.max(...radii); + + const result: PenisStats = { + averageSize: Math.round(averageSize * 100) / 100, + averageRadius: Math.round(averageRadius * 100) / 100, + minSize, + maxSize, + minRadius, + maxRadius, + }; + + if (userId) { + // Get user's latest measurement + const userMeasurement = await ctx + .selectFrom("penis") + .where("userId", "=", userId) + .orderBy("id", "desc") + .selectAll() + .executeTakeFirst(); + + if (userMeasurement) { + result.userSize = userMeasurement.size; + result.userRadius = userMeasurement.radius; + + // Calculate percentiles + const sizePercentile = + (sizes.filter(s => s < userMeasurement.size).length / sizes.length) * 100; + const radiusPercentile = + (radii.filter(r => r < userMeasurement.radius).length / radii.length) * 100; + + result.userSizePercentile = Math.round(sizePercentile * 100) / 100; + result.userRadiusPercentile = Math.round(radiusPercentile * 100) / 100; + } + } + + return result; +} + +/** + * Get boobs statistics + */ +export async function getBoobsStats( + userId?: Snowflake, + ctx: Kysely = db(), +): Promise { + // Get latest measurement for each user using a join with max id subquery + // We use id as a proxy for "latest" since it's auto-incrementing + const maxIdsSubquery = ctx + .selectFrom("boobs") + .select(["userId", ({ fn }) => fn.max("id").as("maxId")]) + .groupBy("userId") + .as("maxIds"); + + const latestMeasurements = await ctx + .selectFrom("boobs") + .innerJoin(maxIdsSubquery, join => + join.onRef("boobs.userId", "=", "maxIds.userId").onRef("boobs.id", "=", "maxIds.maxId"), + ) + .select(["boobs.userId", "boobs.size"]) + .execute(); + + // Calculate statistics from latest measurements + if (latestMeasurements.length === 0) { + return { + averageSize: 0, + minSize: 0, + maxSize: 0, + }; + } + + const sizes = latestMeasurements.map(m => m.size); + + const averageSize = sizes.reduce((a, b) => a + b, 0) / sizes.length; + const minSize = Math.min(...sizes); + const maxSize = Math.max(...sizes); + + const result: BoobsStats = { + averageSize: Math.round(averageSize * 100) / 100, + minSize, + maxSize, + }; + + if (userId) { + // Get user's latest measurement + const userMeasurement = await ctx + .selectFrom("boobs") + .where("userId", "=", userId) + .orderBy("id", "desc") + .selectAll() + .executeTakeFirst(); + + if (userMeasurement) { + result.userSize = userMeasurement.size; + + // Calculate percentile + const sizePercentile = + (sizes.filter(s => s < userMeasurement.size).length / sizes.length) * 100; + result.userSizePercentile = Math.round(sizePercentile * 100) / 100; + } + } + + return result; +} + +/** + * Get most frequently used emote (global) + */ +export async function getMostFrequentEmote( + limit: number = 1, + ctx: Kysely = db(), +): Promise { + const results = await ctx + .selectFrom("emoteUse") + .innerJoin("emote", "emote.id", "emoteUse.emoteId") + .where("emote.deletedAt", "is", null) + .groupBy(["emote.emoteId", "emote.name", "emote.isAnimated"]) + .select([ + "emote.emoteId", + "emote.name as emoteName", + "emote.isAnimated", + ({ fn }) => fn.countAll().as("usageCount"), + ]) + .orderBy(sql`COUNT(*)`, "desc") + .limit(limit) + .execute(); + + return results.map(r => ({ + emoteId: r.emoteId, + emoteName: r.emoteName, + isAnimated: r.isAnimated, + usageCount: r.usageCount, + })); +} + +/** + * Get most frequently used emote by a specific user + */ +export async function getUserMostFrequentEmote( + userId: Snowflake, + limit: number = 1, + ctx: Kysely = db(), +): Promise { + // Note: This assumes we track which user used which emote + // Looking at the emoteUse table, it doesn't seem to track userId + // This would require additional information from messages or reactions + // For now, we'll return an empty array or implement if message context is available + + // If we need this, we'd need to join with message data or reactions + // which might not be directly available in the emoteUse table + // This is a placeholder implementation + return []; +} From 1533908911fdf6d755e8a7ad73b19755a9e470d5 Mon Sep 17 00:00:00 2001 From: holzmaster Date: Thu, 4 Dec 2025 15:21:41 +0100 Subject: [PATCH 2/4] Remove useless function --- src/service/stats.ts | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/src/service/stats.ts b/src/service/stats.ts index 51a6c0b2..a3dcce6c 100644 --- a/src/service/stats.ts +++ b/src/service/stats.ts @@ -381,22 +381,3 @@ export async function getMostFrequentEmote( usageCount: r.usageCount, })); } - -/** - * Get most frequently used emote by a specific user - */ -export async function getUserMostFrequentEmote( - userId: Snowflake, - limit: number = 1, - ctx: Kysely = db(), -): Promise { - // Note: This assumes we track which user used which emote - // Looking at the emoteUse table, it doesn't seem to track userId - // This would require additional information from messages or reactions - // For now, we'll return an empty array or implement if message context is available - - // If we need this, we'd need to join with message data or reactions - // which might not be directly available in the emoteUse table - // This is a placeholder implementation - return []; -} From 0bd50f4b0be5f21057e620e850c662d3e5931e32 Mon Sep 17 00:00:00 2001 From: holzmaster Date: Thu, 4 Dec 2025 15:28:31 +0100 Subject: [PATCH 3/4] Add wrapped command --- src/commands/wrapped.ts | 233 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 233 insertions(+) create mode 100644 src/commands/wrapped.ts diff --git a/src/commands/wrapped.ts b/src/commands/wrapped.ts new file mode 100644 index 00000000..332f5581 --- /dev/null +++ b/src/commands/wrapped.ts @@ -0,0 +1,233 @@ +// +// !CAUTION! this file is entirely vibe-coded and only used for dönerjesus-wrapped +// !CAUTION! It will be deleted in 2026-01-01 +// + +import { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + type CommandInteraction, + ComponentType, + ContainerBuilder, + MessageFlags, + SlashCommandBuilder, + type User, +} from "discord.js"; + +import type { BotContext } from "#context.ts"; +import type { ApplicationCommand } from "#commands/command.ts"; +import { ensureChatInputCommand } from "#utils/interactionUtils.ts"; +import * as statsService from "#service/stats.ts"; +import log from "#log"; + +export default class WrappedCommand implements ApplicationCommand { + name = "wrapped"; + description = "Zeigt deine Jahresstatistiken"; + + applicationCommand = new SlashCommandBuilder() + .setName(this.name) + .setDescription(this.description); + + async handleInteraction(interaction: CommandInteraction, context: BotContext) { + const cmd = ensureChatInputCommand(interaction); + const user = cmd.user; + + await this.#createWrappedView(context, interaction, user); + } + + async #createWrappedView(context: BotContext, interaction: CommandInteraction, user: User) { + // Load all stats in parallel + const [pollStats, inventoryStats, honorStats, penisStats, boobsStats, topEmotes] = + await Promise.all([ + statsService.getPollStats(user.id), + statsService.getInventoryStats(user.id), + statsService.getHonorStats(user.id), + statsService.getPenisStats(user.id), + statsService.getBoobsStats(user.id), + statsService.getMostFrequentEmote(5), + ]); + + const totalPages = 5; + + function buildMessageData(pageIndex: number) { + const container = new ContainerBuilder().addSectionComponents(section => { + section.addTextDisplayComponents(t => + t.setContent(`## 📊 ${user}'s Wrapped ${new Date().getUTCFullYear()}`), + ); + + switch (pageIndex) { + case 0: { + // Umfragen + section.addTextDisplayComponents( + t => t.setContent(`### 📋 Umfragen\n`), + t => + t.setContent( + `Du hast **${pollStats.userPolls ?? 0}** Umfragen erstellt\n` + + `und bei **${pollStats.userVotes ?? 0}** Umfragen abgestimmt\n\n` + + `Insgesamt gibt es **${pollStats.totalPolls}** Umfragen\n` + + `mit **${pollStats.totalVotes}** Stimmen.`, + ), + ); + break; + } + + case 1: { + // Inventar + section.addTextDisplayComponents( + t => t.setContent(`### 🎁 Inventar\n`), + t => + t.setContent( + `Du besitzt **${inventoryStats.itemCount}** Items\n\n` + + `Du bist besser als **${inventoryStats.percentile.toFixed(1)}%** ` + + `aller anderen Nutzer! 🎉`, + ), + ); + break; + } + + case 2: { + // Ehre + section.addTextDisplayComponents( + t => t.setContent(`### ⭐ Ehre\n`), + t => + t.setContent( + `Du hast **${honorStats.collectedPoints.toFixed(1)}** Ehre-Punkte gesammelt\n\n` + + `Du hast **${honorStats.votesGiven}** mal Ehre vergeben\n` + + `und dabei etwa **${honorStats.awardedPoints}** Punkte verteilt.`, + ), + ); + break; + } + + case 3: { + // Penis & Boobs + section.addTextDisplayComponents(t => t.setContent(`### 📏 Messungen\n`)); + + let penisContent: string; + if (penisStats.userSize !== undefined) { + penisContent = + `**Penis:** ${penisStats.userSize}cm (${penisStats.userRadius}cm Radius)\n` + + `Du bist größer als **${penisStats.userSizePercentile?.toFixed(1) ?? 0}%** der anderen\n\n` + + `*Durchschnitt: ${penisStats.averageSize.toFixed(1)}cm (${penisStats.averageRadius.toFixed(1)}cm Radius)*\n` + + `*Min: ${penisStats.minSize}cm | Max: ${penisStats.maxSize}cm*\n\n`; + } else { + penisContent = `**Penis:** Noch nicht gemessen\n\n`; + } + + let boobsContent: string; + if (boobsStats.userSize !== undefined) { + boobsContent = + `**Boobs:** Größe ${boobsStats.userSize}\n` + + `Du bist größer als **${boobsStats.userSizePercentile?.toFixed(1) ?? 0}%** der anderen\n\n` + + `*Durchschnitt: ${boobsStats.averageSize.toFixed(1)}*\n` + + `*Min: ${boobsStats.minSize} | Max: ${boobsStats.maxSize}*`; + } else { + boobsContent = `**Boobs:** Noch nicht gemessen`; + } + + section.addTextDisplayComponents(t => + t.setContent(penisContent + boobsContent), + ); + break; + } + + case 4: { + // Emotes + section.addTextDisplayComponents(t => t.setContent(`### 😀 Emotes\n`)); + + if (topEmotes.length > 0) { + const emoteList = topEmotes + .map((emote, idx) => { + const emoji = context.client.emojis.cache.get(emote.emoteId); + const display = emoji ? `${emoji}` : emote.emoteName; + return `${idx + 1}. ${display} - **${emote.usageCount}** mal verwendet`; + }) + .join("\n"); + + section.addTextDisplayComponents(t => + t.setContent( + `Top 5 häufigste Emotes auf dem Server:\n\n${emoteList}`, + ), + ); + } else { + section.addTextDisplayComponents(t => + t.setContent("Keine Emote-Statistiken verfügbar."), + ); + } + break; + } + + default: { + section.addTextDisplayComponents(t => t.setContent("Ungültige Seite.")); + } + } + + return section; + }); + + return { + components: [ + container, + new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId("wrapped-prev") + .setLabel("← Zurück") + .setStyle(ButtonStyle.Secondary) + .setDisabled(pageIndex <= 0), + new ButtonBuilder() + .setCustomId("wrapped-next") + .setLabel("Weiter →") + .setStyle(ButtonStyle.Secondary) + .setDisabled(pageIndex >= totalPages - 1), + ), + ], + } as const; + } + + let pageIndex = 0; + + const callbackResponse = await interaction.reply({ + ...buildMessageData(pageIndex), + flags: MessageFlags.IsComponentsV2, + withResponse: true, + tts: false, + }); + + const message = callbackResponse.resource?.message; + if (message === null || message === undefined) { + throw new Error("Expected message to be present."); + } + + const collector = message.createMessageComponentCollector({ + componentType: ComponentType.Button, + filter: i => i.user.id === interaction.user.id, + time: 300_000, // 5 minutes + }); + + collector.on("collect", async i => { + i.deferUpdate(); + switch (i.customId) { + case "wrapped-prev": + pageIndex = Math.max(0, pageIndex - 1); + break; + case "wrapped-next": + pageIndex = Math.min(totalPages - 1, pageIndex + 1); + break; + default: + log.warn(`Unknown customId: "${i.customId}"`); + return; + } + + await message.edit({ + ...buildMessageData(pageIndex), + }); + }); + + collector.on("end", async () => { + await message.edit({ + components: [], + }); + }); + } +} From ab674fc65952a5f142576f24d0b50893bf77bfaa Mon Sep 17 00:00:00 2001 From: holzmaster Date: Thu, 4 Dec 2025 15:42:15 +0100 Subject: [PATCH 4/4] Fix components (sections must have accessories) --- src/commands/wrapped.ts | 197 +++++++++++++++++++--------------------- 1 file changed, 93 insertions(+), 104 deletions(-) diff --git a/src/commands/wrapped.ts b/src/commands/wrapped.ts index 332f5581..a56f2697 100644 --- a/src/commands/wrapped.ts +++ b/src/commands/wrapped.ts @@ -51,120 +51,109 @@ export default class WrappedCommand implements ApplicationCommand { const totalPages = 5; function buildMessageData(pageIndex: number) { - const container = new ContainerBuilder().addSectionComponents(section => { - section.addTextDisplayComponents(t => - t.setContent(`## 📊 ${user}'s Wrapped ${new Date().getUTCFullYear()}`), - ); - - switch (pageIndex) { - case 0: { - // Umfragen - section.addTextDisplayComponents( - t => t.setContent(`### 📋 Umfragen\n`), - t => - t.setContent( - `Du hast **${pollStats.userPolls ?? 0}** Umfragen erstellt\n` + - `und bei **${pollStats.userVotes ?? 0}** Umfragen abgestimmt\n\n` + - `Insgesamt gibt es **${pollStats.totalPolls}** Umfragen\n` + - `mit **${pollStats.totalVotes}** Stimmen.`, - ), - ); - break; - } + const container = new ContainerBuilder().addTextDisplayComponents(t => + t.setContent(`## 📊 ${user}'s Wrapped ${new Date().getUTCFullYear()}`), + ); + + switch (pageIndex) { + case 0: { + container.addTextDisplayComponents( + t => t.setContent(`### 📋 Umfragen\n`), + t => + t.setContent( + `Du hast **${pollStats.userPolls ?? 0}** Umfragen erstellt\n` + + `und bei **${pollStats.userVotes ?? 0}** Umfragen abgestimmt\n\n` + + `Insgesamt gibt es **${pollStats.totalPolls}** Umfragen\n` + + `mit **${pollStats.totalVotes}** Stimmen.`, + ), + ); + break; + } - case 1: { - // Inventar - section.addTextDisplayComponents( - t => t.setContent(`### 🎁 Inventar\n`), - t => - t.setContent( - `Du besitzt **${inventoryStats.itemCount}** Items\n\n` + - `Du bist besser als **${inventoryStats.percentile.toFixed(1)}%** ` + - `aller anderen Nutzer! 🎉`, - ), - ); - break; - } + case 1: { + container.addTextDisplayComponents( + t => t.setContent(`### 🎁 Inventar\n`), + t => + t.setContent( + `Du besitzt **${inventoryStats.itemCount}** Items\n\n` + + `Du bist besser als **${inventoryStats.percentile.toFixed(1)}%** ` + + `aller anderen Nutzer! 🎉`, + ), + ); + break; + } - case 2: { - // Ehre - section.addTextDisplayComponents( - t => t.setContent(`### ⭐ Ehre\n`), - t => - t.setContent( - `Du hast **${honorStats.collectedPoints.toFixed(1)}** Ehre-Punkte gesammelt\n\n` + - `Du hast **${honorStats.votesGiven}** mal Ehre vergeben\n` + - `und dabei etwa **${honorStats.awardedPoints}** Punkte verteilt.`, - ), - ); - break; - } + case 2: { + container.addTextDisplayComponents( + t => t.setContent(`### ⭐ Ehre\n`), + t => + t.setContent( + `Du hast **${honorStats.collectedPoints.toFixed(1)}** Ehre-Punkte gesammelt\n\n` + + `Du hast **${honorStats.votesGiven}** mal Ehre vergeben\n` + + `und dabei etwa **${honorStats.awardedPoints}** Punkte verteilt.`, + ), + ); + break; + } - case 3: { - // Penis & Boobs - section.addTextDisplayComponents(t => t.setContent(`### 📏 Messungen\n`)); - - let penisContent: string; - if (penisStats.userSize !== undefined) { - penisContent = - `**Penis:** ${penisStats.userSize}cm (${penisStats.userRadius}cm Radius)\n` + - `Du bist größer als **${penisStats.userSizePercentile?.toFixed(1) ?? 0}%** der anderen\n\n` + - `*Durchschnitt: ${penisStats.averageSize.toFixed(1)}cm (${penisStats.averageRadius.toFixed(1)}cm Radius)*\n` + - `*Min: ${penisStats.minSize}cm | Max: ${penisStats.maxSize}cm*\n\n`; - } else { - penisContent = `**Penis:** Noch nicht gemessen\n\n`; - } - - let boobsContent: string; - if (boobsStats.userSize !== undefined) { - boobsContent = - `**Boobs:** Größe ${boobsStats.userSize}\n` + - `Du bist größer als **${boobsStats.userSizePercentile?.toFixed(1) ?? 0}%** der anderen\n\n` + - `*Durchschnitt: ${boobsStats.averageSize.toFixed(1)}*\n` + - `*Min: ${boobsStats.minSize} | Max: ${boobsStats.maxSize}*`; - } else { - boobsContent = `**Boobs:** Noch nicht gemessen`; - } - - section.addTextDisplayComponents(t => - t.setContent(penisContent + boobsContent), - ); - break; + case 3: { + container.addTextDisplayComponents(t => t.setContent(`### 📏 Messungen\n`)); + + let penisContent: string; + if (penisStats.userSize !== undefined) { + penisContent = + `**Penis:** ${penisStats.userSize}cm (${penisStats.userRadius}cm Radius)\n` + + `Du bist größer als **${penisStats.userSizePercentile?.toFixed(1) ?? 0}%** der anderen\n\n` + + `*Durchschnitt: ${penisStats.averageSize.toFixed(1)}cm (${penisStats.averageRadius.toFixed(1)}cm Radius)*\n` + + `*Min: ${penisStats.minSize}cm | Max: ${penisStats.maxSize}cm*\n\n`; + } else { + penisContent = `**Penis:** Noch nicht gemessen\n\n`; } - case 4: { - // Emotes - section.addTextDisplayComponents(t => t.setContent(`### 😀 Emotes\n`)); - - if (topEmotes.length > 0) { - const emoteList = topEmotes - .map((emote, idx) => { - const emoji = context.client.emojis.cache.get(emote.emoteId); - const display = emoji ? `${emoji}` : emote.emoteName; - return `${idx + 1}. ${display} - **${emote.usageCount}** mal verwendet`; - }) - .join("\n"); - - section.addTextDisplayComponents(t => - t.setContent( - `Top 5 häufigste Emotes auf dem Server:\n\n${emoteList}`, - ), - ); - } else { - section.addTextDisplayComponents(t => - t.setContent("Keine Emote-Statistiken verfügbar."), - ); - } - break; + let boobsContent: string; + if (boobsStats.userSize !== undefined) { + boobsContent = + `**Boobs:** Größe ${boobsStats.userSize}\n` + + `Du bist größer als **${boobsStats.userSizePercentile?.toFixed(1) ?? 0}%** der anderen\n\n` + + `*Durchschnitt: ${boobsStats.averageSize.toFixed(1)}*\n` + + `*Min: ${boobsStats.minSize} | Max: ${boobsStats.maxSize}*`; + } else { + boobsContent = `**Boobs:** Noch nicht gemessen`; } - default: { - section.addTextDisplayComponents(t => t.setContent("Ungültige Seite.")); + container.addTextDisplayComponents(t => + t.setContent(penisContent + boobsContent), + ); + break; + } + + case 4: { + container.addTextDisplayComponents(t => t.setContent(`### 😀 Emotes\n`)); + + if (topEmotes.length > 0) { + const emoteList = topEmotes + .map((emote, idx) => { + const emoji = context.client.emojis.cache.get(emote.emoteId); + const display = emoji ? `${emoji}` : emote.emoteName; + return `${idx + 1}. ${display} - **${emote.usageCount}** mal verwendet`; + }) + .join("\n"); + + container.addTextDisplayComponents(t => + t.setContent(`Top 5 häufigste Emotes auf dem Server:\n\n${emoteList}`), + ); + } else { + container.addTextDisplayComponents(t => + t.setContent("Keine Emote-Statistiken verfügbar."), + ); } + break; } - return section; - }); + default: { + container.addTextDisplayComponents(t => t.setContent("Ungültige Seite.")); + } + } return { components: [