Carl-Bot-Paket: Moderation, Auto-/Sticky-Roles, Level-System, Tags, geplante Posts
- Moderation: /warn (DM + Mod-Log), /warns (Historie + löschen), /timeout (max 28d), /purge (bulk, 14-Tage-Limit sauber gemeldet); Mod-Log erweitert um Join/Leave und Nick-/Rollen-Änderungen - Auto-Rolle bei Join + Sticky-Roles (Rollen bei Leave gesichert, bei Rejoin wiederhergestellt; managed/@everyone ausgenommen) — Rollen-Tab - Level-System (opt-in): 15-25 XP/Nachricht mit 60s-Cooldown, MEE6-Formel, Level-Up-Announce, Rollen-Belohnungen (Level=RolleID), /rank mit Fortschritt, öffentliche Bestenliste /level; Community-Tab - Tags: /tag mit Autocomplete, Verwaltung im Composer-Tab (/api/tags) - Geplante Posts: einmalig/täglich/wöchentlich, Minuten-Scheduler, DST-sicher über Neuberechnung aus Zeitfeldern; Composer-Tab (/api/scheduled) - Autocomplete-Dispatch im InteractionCreate; alles smoke-getestet Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
// 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';
|
||||
|
||||
const XP_COOLDOWN_MS = 60_000;
|
||||
const XP_MIN = 15;
|
||||
const XP_SPREAD = 11; // 15–25 XP pro gewerteter Nachricht
|
||||
|
||||
/** 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 < XP_COOLDOWN_MS) return null;
|
||||
|
||||
const xp = row.xp + XP_MIN + Math.floor(Math.random() * XP_SPREAD);
|
||||
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 */
|
||||
async function grantRewards(member, level) {
|
||||
for (const reward of levelRewards()) {
|
||||
if (reward.level > level) break;
|
||||
if (!member.roles.cache.has(reward.roleId)) {
|
||||
await member.roles.add(reward.roleId).catch((e) =>
|
||||
console.error(`[level] Belohnungs-Rolle ${reward.roleId} fehlgeschlagen: ${e.message}`)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
if (message.member) await grantRewards(message.member, result.level);
|
||||
if (levelsAnnounce()) {
|
||||
await message.channel
|
||||
.send({
|
||||
content: `🎉 **${message.member?.displayName ?? message.author.username}** ist jetzt **Level ${result.level}**!`,
|
||||
allowedMentions: { parse: [] },
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[level] XP-Verarbeitung fehlgeschlagen:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user