Level-Embeds aufgewertet, Server-Footer korrigiert
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:
+37
-12
@@ -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 1–3 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
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user