Level-Embeds aufgewertet, Server-Footer korrigiert
Deploy / check (push) Has been cancelled
Deploy / deploy (push) Has been cancelled

Server-Monitor: Der Footer behauptete fest "alle 2 min", obwohl das Intervall
seit den einstellbaren Werten aus monitor_interval kommt. Wer im Panel etwas
anderes setzt, bekam eine falsche Angabe. Liest jetzt den echten Wert.

Level-Aufstieg: War als einziges im Community-Bereich reiner Text, waehrend
Geburtstage, Verlosungen und Bewerbungen laengst Embeds sind. Jetzt ueber
brandEmbed mit Avatar, Gesamt-XP und Rest bis zum naechsten Level. Der Text
bleibt die Vorlage aus dem Panel — nur der Rahmen ist neu.

Dazu zeigt das Embed die Belohnungs-Rolle: entweder die gerade vergebene oder
die naechste, auf die man hinspielt. grantRewards() gibt dafuer zurueck, was
wirklich dazukam.

Ist die Vorlage im Panel leer, wirft setDescription('') — der Aufstieg waere
still ausgefallen. Faellt jetzt auf eine schlichte Zeile zurueck.

/rank: Avatar als Thumbnail, Podest-Zeichen fuer die ersten drei, Rest-XP und
naechste Belohnung als eigene Felder. Das Feld erscheint nur, wenn es
Belohnungs-Rollen gibt — sonst stuende dort ein leeres Feld.

Nebenbei zwei veraltete ephemeral: true auf flags: MessageFlags.Ephemeral
umgestellt, wie es der Rest der Befehle schon macht.

Geprueft: alle vier Embeds wirklich gebaut und ausgegeben, discord.js
validiert dabei. Das Intervall im Footer zog den auf 7 gestellten Wert.
Die negative XP-Differenz im ersten Testlauf war ein inkonsistenter
Testdatensatz — mit 4200 XP ist man Level 9, nicht 7; ueber 0..60000 XP
durchgespielt wird die Differenz nie negativ.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-01 05:21:56 +02:00
co-authored by Claude Fable 5
parent 8fa43a3e47
commit 2d4afad3cb
3 changed files with 102 additions and 30 deletions
+37 -12
View File
@@ -1,8 +1,12 @@
// /rank — eigenes Level, XP-Fortschritt und Platzierung anzeigen // /rank — eigenes Level, XP-Fortschritt und Platzierung anzeigen
import { SlashCommandBuilder, EmbedBuilder } from 'discord.js'; import { SlashCommandBuilder, MessageFlags } from 'discord.js';
import { getLevelRow, rankOf } from '../../db.js'; import { getLevelRow, rankOf } from '../../db.js';
import { xpForLevel } from '../levels.js'; import { xpForLevel } from '../levels.js';
import { levelsEnabled, publicUrl, brandColor, brandFooter } from '../../runtime-settings.js'; import { brandEmbed } from '../../embeds.js';
import { levelsEnabled, publicUrl, levelRewards } from '../../runtime-settings.js';
// Platz 13 bekommen ein Zeichen, danach schlicht die Nummer
const PODEST = ['🥇', '🥈', '🥉'];
export const data = new SlashCommandBuilder() export const data = new SlashCommandBuilder()
.setName('rank') .setName('rank')
@@ -11,13 +15,19 @@ export const data = new SlashCommandBuilder()
export async function execute(interaction) { export async function execute(interaction) {
if (!levelsEnabled()) { if (!levelsEnabled()) {
await interaction.reply({ content: '❌ Das Level-System ist nicht aktiviert.', ephemeral: true }); await interaction.reply({
content: '❌ Das Level-System ist nicht aktiviert.',
flags: MessageFlags.Ephemeral,
});
return; return;
} }
const user = interaction.options.getUser('member') ?? interaction.user; const user = interaction.options.getUser('member') ?? interaction.user;
const row = getLevelRow(user.id); const row = getLevelRow(user.id);
if (!row) { if (!row) {
await interaction.reply({ content: `**${user.username}** hat noch keine XP — einfach mitschreiben! ✍️`, ephemeral: true }); await interaction.reply({
content: `**${user.username}** hat noch keine XP — einfach mitschreiben! ✍️`,
flags: MessageFlags.Ephemeral,
});
return; return;
} }
@@ -29,15 +39,30 @@ export async function execute(interaction) {
const barLen = 14; const barLen = 14;
const bar = '█'.repeat(Math.round((pct / 100) * barLen)).padEnd(barLen, '·'); const bar = '█'.repeat(Math.round((pct / 100) * barLen)).padEnd(barLen, '·');
await interaction.reply({ const platz = rankOf(user.id);
embeds: [ const zahl = (n) => n.toLocaleString('de-DE');
new EmbedBuilder() const naechste = levelRewards().find((r) => r.level > row.level) ?? null;
.setColor(brandColor())
const embed = brandEmbed(interaction.client, 'LEVEL')
.setAuthor({ name: user.username, iconURL: user.displayAvatarURL({ size: 64 }) }) .setAuthor({ name: user.username, iconURL: user.displayAvatarURL({ size: 64 }) })
.setTitle(`Level ${row.level} · Platz #${rankOf(user.id)}`) .setTitle(`${PODEST[platz - 1] ?? ''} Level ${row.level} · Platz #${platz}`.trim())
.setDescription(`\`${bar}\` ${pct}%\n**${progress} / ${needed} XP** bis Level ${row.level + 1} · gesamt ${row.xp} XP`)
.setURL(`${publicUrl()}/level`) .setURL(`${publicUrl()}/level`)
.setFooter({ text: brandFooter('LEVEL') }), .setThumbnail(user.displayAvatarURL({ size: 128 }))
], .setDescription(`\`${bar}\` **${pct}%**`)
.addFields(
{ name: `Bis Level ${row.level + 1}`, value: `${zahl(needed - progress)} XP`, inline: true },
{ name: 'Gesamt', value: `${zahl(row.xp)} XP`, inline: true },
);
// Nur zeigen, was es auch gibt — ohne Belohnungs-Rollen bliebe hier ein
// leeres Feld stehen und das Embed sähe kaputt aus.
if (naechste) {
embed.addFields({
name: 'Nächste Belohnung',
value: `<@&${naechste.roleId}> ab Level ${naechste.level}`,
inline: true,
}); });
}
await interaction.reply({ embeds: [embed], allowedMentions: { parse: [] } });
} }
+53 -11
View File
@@ -4,6 +4,7 @@ import { Events } from 'discord.js';
import { getLevelRow, setLevelRow } from '../db.js'; import { getLevelRow, setLevelRow } from '../db.js';
import { levelsEnabled, levelsAnnounce, levelRewards } from '../runtime-settings.js'; import { levelsEnabled, levelsAnnounce, levelRewards } from '../runtime-settings.js';
import { renderTemplate } from '../templates.js'; import { renderTemplate } from '../templates.js';
import { brandEmbed } from '../embeds.js';
import { tuningMs, xpRange } from '../tuning.js'; import { tuningMs, xpRange } from '../tuning.js';
/** Kumulierte XP-Schwelle, ab der ein Level erreicht ist (MEE6-Formel pro Stufe) */ /** Kumulierte XP-Schwelle, ab der ein Level erreicht ist (MEE6-Formel pro Stufe) */
@@ -35,16 +36,28 @@ export function awardXp(userId, username, now = Date.now()) {
return { xp, level, leveledUp }; return { xp, level, leveledUp };
} }
/** Beim Level-Up fällige Belohnungs-Rollen (alle Schwellen ≤ Level) vergeben */ /**
* Beim Level-Up fällige Belohnungs-Rollen (alle Schwellen ≤ Level) vergeben.
* @returns {Promise<string[]>} Rollen-IDs, die neu dazukamen — fürs Embed
*/
async function grantRewards(member, level) { async function grantRewards(member, level) {
const neu = [];
for (const reward of levelRewards()) { for (const reward of levelRewards()) {
if (reward.level > level) break; if (reward.level > level) break;
if (!member.roles.cache.has(reward.roleId)) { if (!member.roles.cache.has(reward.roleId)) {
await member.roles.add(reward.roleId).catch((e) => const ok = await member.roles.add(reward.roleId).then(() => true).catch((e) => {
console.error(`[level] Belohnungs-Rolle ${reward.roleId} fehlgeschlagen: ${e.message}`) console.error(`[level] Belohnungs-Rolle ${reward.roleId} fehlgeschlagen: ${e.message}`);
); return false;
});
if (ok) neu.push(reward.roleId);
} }
} }
return neu;
}
/** Nächste Belohnungs-Stufe oberhalb des aktuellen Levels (oder null) */
function naechsteBelohnung(level) {
return levelRewards().find((r) => r.level > level) ?? null;
} }
export function registerLevels(client) { export function registerLevels(client) {
@@ -59,19 +72,48 @@ export function registerLevels(client) {
); );
if (!result?.leveledUp) return; if (!result?.leveledUp) return;
if (message.member) await grantRewards(message.member, result.level); const belohnt = message.member ? await grantRewards(message.member, result.level) : [];
if (levelsAnnounce()) { if (levelsAnnounce()) {
const name = message.member?.displayName ?? message.author.username; const name = message.member?.displayName ?? message.author.username;
await message.channel const bisNaechstes = xpForLevel(result.level + 1) - result.xp;
.send({ const naechste = naechsteBelohnung(result.level);
content: renderTemplate('level.up', {
// Der Text bleibt die Vorlage aus dem Panel — nur der Rahmen ist
// jetzt ein Embed, damit es zu Geburtstagen und Verlosungen passt.
// Leere Vorlage: discord.js wirft bei setDescription('') und der
// Aufstieg fiele still aus. Lieber eine schlichte Zeile.
const text = renderTemplate('level.up', {
user: name, user: name,
mention: `<@${message.author.id}>`, mention: `<@${message.author.id}>`,
level: result.level, level: result.level,
xp: result.xp, xp: result.xp,
}), }).trim() || `**${name}** ist jetzt **Level ${result.level}**!`;
allowedMentions: { parse: [] },
}) const embed = brandEmbed(client, 'LEVEL')
.setDescription(text)
.setThumbnail(message.author.displayAvatarURL({ size: 128 }))
.addFields(
{ name: 'Gesamt', value: `${result.xp.toLocaleString('de-DE')} XP`, inline: true },
{ name: `Bis Level ${result.level + 1}`, value: `${bisNaechstes.toLocaleString('de-DE')} XP`, inline: true },
);
if (belohnt.length > 0) {
embed.addFields({
name: belohnt.length > 1 ? 'Neue Rollen' : 'Neue Rolle',
value: belohnt.map((id) => `<@&${id}>`).join(' '),
inline: true,
});
} else if (naechste) {
embed.addFields({
name: 'Nächste Belohnung',
value: `<@&${naechste.roleId}> ab Level ${naechste.level}`,
inline: true,
});
}
await message.channel
// Rollen-Erwähnungen im Embed pingen ohnehin nicht — der
// Schalter hier hält auch den Nachrichtentext still.
.send({ embeds: [embed], allowedMentions: { parse: [] } })
.catch(() => {}); .catch(() => {});
} }
} catch (error) { } catch (error) {
+6 -1
View File
@@ -84,7 +84,12 @@ export function buildServerEmbed(r) {
.setColor(r.online ? GREEN : RED) .setColor(r.online ? GREEN : RED)
.setTitle(`${icon} ${r.name}`) .setTitle(`${icon} ${r.name}`)
.addFields({ name: 'Status', value: r.online ? '🟢 **Online**' : '🔴 **Offline**', inline: true }) .addFields({ name: 'Status', value: r.online ? '🟢 **Online**' : '🔴 **Offline**', inline: true })
.setFooter({ text: `${brandFooter('STATUS')}${r.ping != null ? `${r.ping}ms` : ''} • alle 2 min` }) // Intervall aus den Werten lesen, nicht festschreiben — sonst behauptet
// das Embed weiter „alle 2 min", nachdem im Panel etwas anderes steht.
.setFooter({
text: `${brandFooter('STATUS')}${r.ping != null ? `${r.ping}ms` : ''}`
+ ` • alle ${tuning('monitor_interval')} min`,
})
.setTimestamp(); .setTimestamp();
if (r.image_url) embed.setThumbnail(r.image_url); if (r.image_url) embed.setThumbnail(r.image_url);