// Level-System (Carl-/MEE6-Stil): XP pro Nachricht mit Cooldown, Level-Up-Announce, // Rollen-Belohnungen; Bestenliste öffentlich auf der Webseite (/level). import { Events } from 'discord.js'; import { getLevelRow, setLevelRow } from '../db.js'; import { levelsEnabled, levelsAnnounce, levelRewards } from '../runtime-settings.js'; import { renderTemplate } from '../templates.js'; import { brandEmbed } from '../embeds.js'; import { tuningMs, xpRange } from '../tuning.js'; /** Kumulierte XP-Schwelle, ab der ein Level erreicht ist (MEE6-Formel pro Stufe) */ export function xpForLevel(level) { let total = 0; for (let n = 0; n < level; n++) total += 5 * n * n + 50 * n + 100; return total; } export function levelFromXp(xp) { let level = 0; while (xp >= xpForLevel(level + 1)) level++; return level; } /** * XP für eine Nachricht gutschreiben (Cooldown-gesteuert). * @returns {{ xp: number, level: number, leveledUp: boolean } | null} null = Cooldown aktiv */ export function awardXp(userId, username, now = Date.now()) { const row = getLevelRow(userId) ?? { user_id: userId, username, xp: 0, level: 0, last_xp_ms: 0 }; if (now - row.last_xp_ms < tuningMs.seconds('xp_cooldown')) return null; const { min, max } = xpRange(); const xp = row.xp + min + Math.floor(Math.random() * (max - min + 1)); const level = levelFromXp(xp); const leveledUp = level > row.level; setLevelRow({ user_id: userId, username, xp, level, last_xp_ms: now }); return { xp, level, leveledUp }; } /** * Beim Level-Up fällige Belohnungs-Rollen (alle Schwellen ≤ Level) vergeben. * @returns {Promise} Rollen-IDs, die neu dazukamen — fürs Embed */ async function grantRewards(member, level) { const neu = []; for (const reward of levelRewards()) { if (reward.level > level) break; if (!member.roles.cache.has(reward.roleId)) { const ok = await member.roles.add(reward.roleId).then(() => true).catch((e) => { 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) { client.on(Events.MessageCreate, async (message) => { try { if (!levelsEnabled()) return; if (!message.guildId || message.author?.bot || message.webhookId) return; const result = awardXp( message.author.id, message.member?.displayName ?? message.author.username ); if (!result?.leveledUp) return; const belohnt = message.member ? await grantRewards(message.member, result.level) : []; if (levelsAnnounce()) { const name = message.member?.displayName ?? message.author.username; const bisNaechstes = xpForLevel(result.level + 1) - result.xp; const naechste = naechsteBelohnung(result.level); // 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, mention: `<@${message.author.id}>`, level: result.level, xp: result.xp, }).trim() || `**${name}** ist jetzt **Level ${result.level}**!`; 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 (error) { console.error('[level] XP-Verarbeitung fehlgeschlagen:', error); } }); }