Implement leveling and economy features with associated models and commands
- Added new models for leveling and economy functionalities in the Prisma schema, including LevelingConfig, MemberLevel, LevelReward, EconomyConfig, MemberEconomy, ShopItem, and InventoryItem. - Introduced commands for managing XP, economy transactions, and shop interactions, enhancing user engagement. - Updated job handling to include reminders and poll closing functionalities. - Enhanced localization support for new commands and features in both German and English. - Improved the bot's job processing capabilities with a dedicated reminder worker for scheduled tasks.
This commit is contained in:
@@ -274,6 +274,414 @@ export const commandLocales = {
|
||||
'verify.panel.options.channel': {
|
||||
de: 'Optionaler Kanal für das Panel',
|
||||
en: 'Optional channel for the panel'
|
||||
},
|
||||
'leveling.rank.description': {
|
||||
de: 'Rangkarte eines Mitglieds anzeigen',
|
||||
en: 'Show a member rank card'
|
||||
},
|
||||
'leveling.rank.options.user': {
|
||||
de: 'Mitglied (Standard: du)',
|
||||
en: 'Member (defaults to you)'
|
||||
},
|
||||
'leveling.leaderboard.description': {
|
||||
de: 'Top 10 Mitglieder nach XP',
|
||||
en: 'Top 10 members by XP'
|
||||
},
|
||||
'leveling.xp.description': {
|
||||
de: 'XP verwalten (Admin)',
|
||||
en: 'Manage XP (admin)'
|
||||
},
|
||||
'leveling.xp.give.description': {
|
||||
de: 'XP vergeben',
|
||||
en: 'Give XP'
|
||||
},
|
||||
'leveling.xp.remove.description': {
|
||||
de: 'XP entfernen',
|
||||
en: 'Remove XP'
|
||||
},
|
||||
'leveling.xp.reset.description': {
|
||||
de: 'XP zurücksetzen',
|
||||
en: 'Reset XP'
|
||||
},
|
||||
'leveling.xp.options.user': {
|
||||
de: 'Zielmitglied',
|
||||
en: 'Target member'
|
||||
},
|
||||
'leveling.xp.options.amount': {
|
||||
de: 'XP-Betrag',
|
||||
en: 'XP amount'
|
||||
},
|
||||
'economy.balance.description': {
|
||||
de: 'Kontostand anzeigen',
|
||||
en: 'Show balance'
|
||||
},
|
||||
'economy.daily.description': {
|
||||
de: 'Tägliche Belohnung abholen',
|
||||
en: 'Claim daily reward'
|
||||
},
|
||||
'economy.weekly.description': {
|
||||
de: 'Wöchentliche Belohnung abholen',
|
||||
en: 'Claim weekly reward'
|
||||
},
|
||||
'economy.work.description': {
|
||||
de: 'Arbeiten und Geld verdienen',
|
||||
en: 'Work to earn money'
|
||||
},
|
||||
'economy.pay.description': {
|
||||
de: 'Geld an einen Nutzer senden',
|
||||
en: 'Send money to a user'
|
||||
},
|
||||
'economy.gamble.description': {
|
||||
de: '50/50 um deinen Einsatz spielen',
|
||||
en: '50/50 gamble your bet'
|
||||
},
|
||||
'economy.slots.description': {
|
||||
de: 'Spielautomaten spielen',
|
||||
en: 'Play the slot machine'
|
||||
},
|
||||
'economy.blackjack.description': {
|
||||
de: 'Blackjack spielen',
|
||||
en: 'Play blackjack'
|
||||
},
|
||||
'economy.coinflip.description': {
|
||||
de: 'Kopf oder Zahl um Geld wetten',
|
||||
en: 'Bet on heads or tails'
|
||||
},
|
||||
'economy.shop.description': {
|
||||
de: 'Server-Shop',
|
||||
en: 'Server shop'
|
||||
},
|
||||
'economy.shop.view.description': {
|
||||
de: 'Shop-Artikel anzeigen',
|
||||
en: 'View shop items'
|
||||
},
|
||||
'economy.shop.buy.description': {
|
||||
de: 'Artikel kaufen',
|
||||
en: 'Buy an item'
|
||||
},
|
||||
'economy.shop.options.item_id': {
|
||||
de: 'Artikel-ID aus /shop view',
|
||||
en: 'Item ID from /shop view'
|
||||
},
|
||||
'economy.inventory.description': {
|
||||
de: 'Dein Inventar anzeigen',
|
||||
en: 'Show your inventory'
|
||||
},
|
||||
'economy.eco.description': {
|
||||
de: 'Economy-Verwaltung',
|
||||
en: 'Economy management'
|
||||
},
|
||||
'economy.eco.leaderboard.description': {
|
||||
de: 'Reichsten Nutzer anzeigen',
|
||||
en: 'Show richest users'
|
||||
},
|
||||
'economy.eco.give.description': {
|
||||
de: 'Geld vergeben (Admin)',
|
||||
en: 'Give money (admin)'
|
||||
},
|
||||
'economy.eco.remove.description': {
|
||||
de: 'Geld entfernen (Admin)',
|
||||
en: 'Remove money (admin)'
|
||||
},
|
||||
'economy.eco.reset.description': {
|
||||
de: 'Nutzer-Economy zurücksetzen (Admin)',
|
||||
en: 'Reset user economy (admin)'
|
||||
},
|
||||
'economy.common.options.amount': {
|
||||
de: 'Betrag',
|
||||
en: 'Amount'
|
||||
},
|
||||
'economy.common.options.user': {
|
||||
de: 'Nutzer',
|
||||
en: 'User'
|
||||
},
|
||||
'economy.common.options.userOptional': {
|
||||
de: 'Optionaler Nutzer (Standard: du)',
|
||||
en: 'Optional user (defaults to you)'
|
||||
},
|
||||
'economy.common.options.target': {
|
||||
de: 'Zielnutzer',
|
||||
en: 'Target user'
|
||||
},
|
||||
'economy.coinflip.options.choice': {
|
||||
de: 'Kopf oder Zahl',
|
||||
en: 'Heads or tails'
|
||||
},
|
||||
'economy.choice.heads': {
|
||||
de: 'Kopf',
|
||||
en: 'Heads'
|
||||
},
|
||||
'economy.choice.tails': {
|
||||
de: 'Zahl',
|
||||
en: 'Tails'
|
||||
},
|
||||
'utility.userinfo.description': {
|
||||
de: 'Nutzerinformationen anzeigen',
|
||||
en: 'Show user information'
|
||||
},
|
||||
'utility.serverinfo.description': {
|
||||
de: 'Serverinformationen anzeigen',
|
||||
en: 'Show server information'
|
||||
},
|
||||
'utility.roleinfo.description': {
|
||||
de: 'Rolleninformationen anzeigen',
|
||||
en: 'Show role information'
|
||||
},
|
||||
'utility.roleinfo.options.role': {
|
||||
de: 'Rolle',
|
||||
en: 'Role'
|
||||
},
|
||||
'utility.channelinfo.description': {
|
||||
de: 'Kanalinformationen anzeigen',
|
||||
en: 'Show channel information'
|
||||
},
|
||||
'utility.avatar.description': {
|
||||
de: 'Avatar anzeigen',
|
||||
en: 'Show avatar'
|
||||
},
|
||||
'utility.banner.description': {
|
||||
de: 'Banner anzeigen',
|
||||
en: 'Show banner'
|
||||
},
|
||||
'utility.common.options.userOptional': {
|
||||
de: 'Optionaler Nutzer (Standard: du)',
|
||||
en: 'Optional user (defaults to you)'
|
||||
},
|
||||
'utility.common.options.channelOptional': {
|
||||
de: 'Optionaler Kanal (Standard: aktueller Kanal)',
|
||||
en: 'Optional channel (defaults to current)'
|
||||
},
|
||||
'utility.common.options.attachment': {
|
||||
de: 'Bild-Anhang',
|
||||
en: 'Image attachment'
|
||||
},
|
||||
'utility.common.options.url': {
|
||||
de: 'Bild-URL',
|
||||
en: 'Image URL'
|
||||
},
|
||||
'utility.common.options.name': {
|
||||
de: 'Name',
|
||||
en: 'Name'
|
||||
},
|
||||
'utility.poll.description': {
|
||||
de: 'Umfragen erstellen',
|
||||
en: 'Create polls'
|
||||
},
|
||||
'utility.poll.create.description': {
|
||||
de: 'Neue Umfrage erstellen',
|
||||
en: 'Create a new poll'
|
||||
},
|
||||
'utility.poll.options.question': {
|
||||
de: 'Frage',
|
||||
en: 'Question'
|
||||
},
|
||||
'utility.poll.options.options': {
|
||||
de: 'Optionen (kommagetrennt, 2–5)',
|
||||
en: 'Options (comma-separated, 2–5)'
|
||||
},
|
||||
'utility.poll.options.multiselect': {
|
||||
de: 'Mehrfachauswahl erlauben',
|
||||
en: 'Allow multi-select'
|
||||
},
|
||||
'utility.poll.options.anonymous': {
|
||||
de: 'Anonyme Abstimmung',
|
||||
en: 'Anonymous voting'
|
||||
},
|
||||
'utility.poll.options.duration': {
|
||||
de: 'Optionale Dauer (z. B. 1h)',
|
||||
en: 'Optional duration (e.g. 1h)'
|
||||
},
|
||||
'utility.remindme.description': {
|
||||
de: 'Erinnerung setzen',
|
||||
en: 'Set a reminder'
|
||||
},
|
||||
'utility.remindme.options.when': {
|
||||
de: 'Wann (z. B. 30m oder ISO-Datum)',
|
||||
en: 'When (e.g. 30m or ISO datetime)'
|
||||
},
|
||||
'utility.remindme.options.content': {
|
||||
de: 'Erinnerungstext',
|
||||
en: 'Reminder content'
|
||||
},
|
||||
'utility.remindme.options.recurring_cron': {
|
||||
de: 'Optional: Cron für wiederkehrende Erinnerung',
|
||||
en: 'Optional cron for recurring reminder'
|
||||
},
|
||||
'utility.reminders.description': {
|
||||
de: 'Erinnerungen verwalten',
|
||||
en: 'Manage reminders'
|
||||
},
|
||||
'utility.reminders.list.description': {
|
||||
de: 'Erinnerungen auflisten',
|
||||
en: 'List reminders'
|
||||
},
|
||||
'utility.reminders.delete.description': {
|
||||
de: 'Erinnerung löschen',
|
||||
en: 'Delete a reminder'
|
||||
},
|
||||
'utility.reminders.options.id': {
|
||||
de: 'Erinnerungs-ID (Präfix reicht)',
|
||||
en: 'Reminder ID (prefix is enough)'
|
||||
},
|
||||
'utility.afk.description': {
|
||||
de: 'AFK-Status setzen',
|
||||
en: 'Set AFK status'
|
||||
},
|
||||
'utility.afk.set.description': {
|
||||
de: 'Als AFK markieren',
|
||||
en: 'Mark as AFK'
|
||||
},
|
||||
'utility.afk.options.reason': {
|
||||
de: 'Optionaler AFK-Grund',
|
||||
en: 'Optional AFK reason'
|
||||
},
|
||||
'utility.emoji.description': {
|
||||
de: 'Emojis verwalten',
|
||||
en: 'Manage emojis'
|
||||
},
|
||||
'utility.emoji.add.description': {
|
||||
de: 'Emoji hinzufügen',
|
||||
en: 'Add emoji'
|
||||
},
|
||||
'utility.emoji.remove.description': {
|
||||
de: 'Emoji entfernen',
|
||||
en: 'Remove emoji'
|
||||
},
|
||||
'utility.emoji.steal.description': {
|
||||
de: 'Emoji aus Nachricht übernehmen',
|
||||
en: 'Steal emoji from message'
|
||||
},
|
||||
'utility.emoji.options.emoji': {
|
||||
de: 'Emoji',
|
||||
en: 'Emoji'
|
||||
},
|
||||
'utility.emoji.options.message_id': {
|
||||
de: 'Nachrichten-ID',
|
||||
en: 'Message ID'
|
||||
},
|
||||
'utility.emoji.options.url': {
|
||||
de: 'Emoji-URL',
|
||||
en: 'Emoji URL'
|
||||
},
|
||||
'utility.sticker.description': {
|
||||
de: 'Sticker verwalten',
|
||||
en: 'Manage stickers'
|
||||
},
|
||||
'utility.sticker.add.description': {
|
||||
de: 'Sticker hinzufügen',
|
||||
en: 'Add sticker'
|
||||
},
|
||||
'utility.sticker.options.url': {
|
||||
de: 'Sticker-URL',
|
||||
en: 'Sticker URL'
|
||||
},
|
||||
'utility.timestamp.description': {
|
||||
de: 'Discord-Timestamp-Formate generieren',
|
||||
en: 'Generate Discord timestamp formats'
|
||||
},
|
||||
'utility.timestamp.options.datetime': {
|
||||
de: 'Datum/Uhrzeit (ISO oder relative Dauer)',
|
||||
en: 'Datetime (ISO or relative duration)'
|
||||
},
|
||||
'utility.snipe.description': {
|
||||
de: 'Zuletzt gelöschte Nachricht anzeigen',
|
||||
en: 'Show last deleted message'
|
||||
},
|
||||
'utility.editsnipe.description': {
|
||||
de: 'Zuletzt bearbeitete Nachricht anzeigen',
|
||||
en: 'Show last edited message'
|
||||
},
|
||||
'utility.embed.description': {
|
||||
de: 'Embeds erstellen',
|
||||
en: 'Create embeds'
|
||||
},
|
||||
'utility.embed.builder.description': {
|
||||
de: 'Embed per Modal bauen und senden',
|
||||
en: 'Build and send an embed via modal'
|
||||
},
|
||||
'fun.8ball.description': {
|
||||
de: 'Magische 8-Ball-Antwort auf deine Frage',
|
||||
en: 'Magic 8-ball answer to your question'
|
||||
},
|
||||
'fun.8ball.options.question': {
|
||||
de: 'Deine Frage',
|
||||
en: 'Your question'
|
||||
},
|
||||
'fun.dice.description': {
|
||||
de: 'Würfel werfen',
|
||||
en: 'Roll dice'
|
||||
},
|
||||
'fun.dice.options.sides': {
|
||||
de: 'Anzahl Seiten (Standard: 6)',
|
||||
en: 'Number of sides (default: 6)'
|
||||
},
|
||||
'fun.dice.options.count': {
|
||||
de: 'Anzahl Würfe (Standard: 1, max. 10)',
|
||||
en: 'Number of rolls (default: 1, max 10)'
|
||||
},
|
||||
'fun.flip.description': {
|
||||
de: 'Kostenloser Münzwurf (Kopf oder Zahl)',
|
||||
en: 'Free coin flip (heads or tails)'
|
||||
},
|
||||
'fun.rps.description': {
|
||||
de: 'Schere, Stein, Papier gegen den Bot',
|
||||
en: 'Rock, paper, scissors against the bot'
|
||||
},
|
||||
'fun.rps.options.choice': {
|
||||
de: 'Deine Wahl',
|
||||
en: 'Your choice'
|
||||
},
|
||||
'fun.rps.choice.rock': {
|
||||
de: 'Stein',
|
||||
en: 'Rock'
|
||||
},
|
||||
'fun.rps.choice.paper': {
|
||||
de: 'Papier',
|
||||
en: 'Paper'
|
||||
},
|
||||
'fun.rps.choice.scissors': {
|
||||
de: 'Schere',
|
||||
en: 'Scissors'
|
||||
},
|
||||
'fun.choose.description': {
|
||||
de: 'Zufällige Auswahl aus Optionen',
|
||||
en: 'Random choice from options'
|
||||
},
|
||||
'fun.choose.options.options': {
|
||||
de: 'Optionen (kommagetrennt)',
|
||||
en: 'Options (comma-separated)'
|
||||
},
|
||||
'fun.trivia.description': {
|
||||
de: 'Trivia-Frage mit Multiple Choice',
|
||||
en: 'Trivia question with multiple choice'
|
||||
},
|
||||
'fun.tictactoe.description': {
|
||||
de: 'Tic-Tac-Toe gegen einen anderen Nutzer',
|
||||
en: 'Play Tic-Tac-Toe against another user'
|
||||
},
|
||||
'fun.connect4.description': {
|
||||
de: 'Connect 4 gegen einen anderen Nutzer',
|
||||
en: 'Play Connect 4 against another user'
|
||||
},
|
||||
'fun.hangman.description': {
|
||||
de: 'Hangman mit Buchstaben-Buttons',
|
||||
en: 'Hangman with letter buttons'
|
||||
},
|
||||
'fun.meme.description': {
|
||||
de: 'Zufälliges Meme laden',
|
||||
en: 'Fetch a random meme'
|
||||
},
|
||||
'fun.cat.description': {
|
||||
de: 'Zufälliges Katzenbild laden',
|
||||
en: 'Fetch a random cat image'
|
||||
},
|
||||
'fun.dog.description': {
|
||||
de: 'Zufälliges Hundebild laden',
|
||||
en: 'Fetch a random dog image'
|
||||
},
|
||||
'fun.common.options.opponent': {
|
||||
de: 'Gegner',
|
||||
en: 'Opponent'
|
||||
}
|
||||
} as const;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ export * from './audit.js';
|
||||
export * from './command-locales.js';
|
||||
export * from './discord-localization.js';
|
||||
export * from './phase2.js';
|
||||
export * from './phase3.js';
|
||||
|
||||
export const LocaleSchema = z.enum(['de', 'en']);
|
||||
export type Locale = z.infer<typeof LocaleSchema>;
|
||||
@@ -92,7 +93,152 @@ const de: Dictionary = {
|
||||
'verification.error.generic': 'Verifizierung fehlgeschlagen.',
|
||||
'verification.captcha.success': 'Captcha bestanden. Du wurdest verifiziert.',
|
||||
'verification.captcha.invalid': 'Captcha ungültig oder abgelaufen.',
|
||||
'verification.captcha.title': 'Nexumi Verifizierung'
|
||||
'verification.captcha.title': 'Nexumi Verifizierung',
|
||||
'leveling.disabled': 'Leveling ist auf diesem Server deaktiviert.',
|
||||
'leveling.rank.title': 'Rangkarte',
|
||||
'leveling.leaderboard.title': 'Leaderboard',
|
||||
'leveling.leaderboard.empty': 'Noch keine XP-Daten vorhanden.',
|
||||
'leveling.leaderboard.entry': '{rank}. {user} — Level {level} ({xp} XP)',
|
||||
'leveling.leaderboard.footer': 'Top {count} Mitglieder',
|
||||
'leveling.levelUp.default': 'Glückwunsch {user}, du hast Level {level} erreicht!',
|
||||
'leveling.xp.give.success': '{amount} XP an {user} vergeben. Neues Level: {level}.',
|
||||
'leveling.xp.remove.success': '{amount} XP von {user} entfernt. Neues Level: {level}.',
|
||||
'leveling.xp.reset.success': 'XP von {user} zurückgesetzt.',
|
||||
'leveling.xp.invalidAmount': 'Der Betrag muss größer als 0 sein.',
|
||||
'leveling.user.notFound': 'Nutzer nicht auf diesem Server gefunden.',
|
||||
'economy.error.disabled': 'Das Economy-Modul ist auf diesem Server deaktiviert.',
|
||||
'economy.error.insufficient': 'Du hast nicht genug Geld.',
|
||||
'economy.error.cooldown': 'Cooldown aktiv. Versuche es in {seconds} Sekunden erneut.',
|
||||
'economy.error.invalid_amount': 'Ungültiger Betrag.',
|
||||
'economy.error.self_transfer': 'Du kannst dir nicht selbst Geld senden.',
|
||||
'economy.error.not_found': 'Eintrag nicht gefunden.',
|
||||
'economy.error.out_of_stock': 'Dieser Artikel ist ausverkauft.',
|
||||
'economy.error.role_assign_failed': 'Kauf erfolgreich, aber die Rolle konnte nicht vergeben werden.',
|
||||
'economy.error.game_in_progress': 'Du hast bereits ein laufendes Blackjack-Spiel.',
|
||||
'economy.balance.result': 'Kontostand von {user}: {amount}',
|
||||
'economy.daily.success': 'Tägliche Belohnung: {amount}. Neuer Kontostand: {balance}',
|
||||
'economy.weekly.success': 'Wöchentliche Belohnung: {amount}. Neuer Kontostand: {balance}',
|
||||
'economy.work.success': 'Du hast {amount} verdient. Neuer Kontostand: {balance}',
|
||||
'economy.pay.success': 'Du hast {amount} an {user} gesendet. Dein Kontostand: {balance}',
|
||||
'economy.gamble.win': 'Gewonnen! +{amount}. Kontostand: {balance}',
|
||||
'economy.gamble.lose': 'Verloren! -{amount}. Kontostand: {balance}',
|
||||
'economy.slots.win': '{reels}\nGewinn: {payout}. Kontostand: {balance}',
|
||||
'economy.slots.lose': '{reels}\nKein Gewinn. Kontostand: {balance}',
|
||||
'economy.coinflip.win': '{result} – du hattest {choice} gewählt. Gewonnen! +{amount}. Kontostand: {balance}',
|
||||
'economy.coinflip.lose': '{result} – du hattest {choice} gewählt. Verloren! -{amount}. Kontostand: {balance}',
|
||||
'economy.choice.heads': 'Kopf',
|
||||
'economy.choice.tails': 'Zahl',
|
||||
'economy.shop.empty': 'Der Shop ist leer.',
|
||||
'economy.shop.noDescription': 'Keine Beschreibung',
|
||||
'economy.shop.itemLine': '**{name}** (`{id}`) – {price} | Bestand: {stock}\n{description}',
|
||||
'economy.shop.purchased': 'Du hast **{name}** gekauft. Kontostand: {balance}',
|
||||
'economy.inventory.empty': 'Dein Inventar ist leer.',
|
||||
'economy.inventory.itemLine': '- {name} × {quantity}',
|
||||
'economy.leaderboard.empty': 'Noch keine Einträge auf der Bestenliste.',
|
||||
'economy.leaderboard.line': '{rank}. {user} – {amount}',
|
||||
'economy.admin.give': '{amount} an {user} vergeben. Neuer Kontostand: {balance}',
|
||||
'economy.admin.remove': '{amount} von {user} entfernt. Neuer Kontostand: {balance}',
|
||||
'economy.admin.reset': 'Economy-Daten von {user} zurückgesetzt.',
|
||||
'economy.blackjack.title': 'Blackjack',
|
||||
'economy.blackjack.hand': 'Einsatz: {bet}\n\n**Du:** {playerHand} ({playerTotal})\n**Dealer:** {dealerHand} ({dealerTotal})',
|
||||
'economy.blackjack.button.hit': 'Hit',
|
||||
'economy.blackjack.button.stand': 'Stand',
|
||||
'economy.blackjack.outcome.win': 'Du gewinnst {bet}!',
|
||||
'economy.blackjack.outcome.lose': 'Du verlierst deinen Einsatz.',
|
||||
'economy.blackjack.outcome.push': 'Unentschieden – Einsatz zurück.',
|
||||
'economy.blackjack.outcome.blackjack': 'Blackjack! Du gewinnst {bet}!',
|
||||
'economy.blackjack.expired': 'Dieses Blackjack-Spiel ist abgelaufen oder existiert nicht mehr.',
|
||||
'economy.blackjack.notOwner': 'Nur der Spieler kann diese Buttons nutzen.',
|
||||
'utility.error.invalid_when': 'Ungültiger Zeitpunkt. Nutze z. B. 30m oder ein ISO-Datum in der Zukunft.',
|
||||
'utility.error.invalid_datetime': 'Ungültiges Datum/Uhrzeit.',
|
||||
'utility.error.invalid_poll_options': 'Es werden 2–5 Optionen benötigt (kommagetrennt).',
|
||||
'utility.error.channel_not_found': 'Kanal nicht gefunden.',
|
||||
'utility.error.role_not_found': 'Rolle nicht gefunden.',
|
||||
'utility.error.poll_not_found': 'Umfrage nicht gefunden.',
|
||||
'utility.error.reminder_channel_unavailable': 'Erinnerungskanal nicht erreichbar.',
|
||||
'utility.poll.created': 'Umfrage erstellt.',
|
||||
'utility.poll.closed': 'Diese Umfrage ist geschlossen.',
|
||||
'utility.poll.notFound': 'Umfrage nicht gefunden.',
|
||||
'utility.poll.invalidOption': 'Ungültige Option.',
|
||||
'utility.poll.alreadyVoted': 'Du hast bereits abgestimmt.',
|
||||
'utility.poll.voted': 'Stimme für **{option}** gezählt.',
|
||||
'utility.poll.votes': 'Stimmen',
|
||||
'utility.poll.endsAt': 'Endet {time}',
|
||||
'utility.poll.multiSelect': 'Mehrfachauswahl',
|
||||
'utility.poll.multiSelectHint': 'Du kannst mehrere Optionen wählen.',
|
||||
'utility.remindme.created': 'Erinnerung erstellt (ID: `{id}`).',
|
||||
'utility.reminders.none': 'Keine Erinnerungen.',
|
||||
'utility.reminders.line': '`{id}` — {time}: {content}',
|
||||
'utility.reminders.deleted': 'Erinnerung gelöscht.',
|
||||
'utility.reminders.notFound': 'Erinnerung nicht gefunden.',
|
||||
'utility.afk.set': 'AFK gesetzt. Grund: {reason}',
|
||||
'utility.afk.welcomeBack': 'Willkommen zurück! Du warst AFK: {reason}',
|
||||
'utility.afk.mention': '{user} ist AFK: {reason}',
|
||||
'utility.avatar.title': 'Avatar von {user}',
|
||||
'utility.banner.title': 'Banner von {user}',
|
||||
'utility.banner.none': 'Kein Banner gesetzt.',
|
||||
'utility.emoji.noSource': 'Anhang oder URL erforderlich.',
|
||||
'utility.emoji.added': 'Emoji {emoji} hinzugefügt.',
|
||||
'utility.emoji.removed': 'Emoji entfernt.',
|
||||
'utility.emoji.notFound': 'Emoji nicht gefunden.',
|
||||
'utility.emoji.messageNotFound': 'Nachricht nicht gefunden.',
|
||||
'utility.emoji.noEmojiInMessage': 'Kein Custom-Emoji in der Nachricht.',
|
||||
'utility.emoji.stolen': 'Emoji {emoji} gestohlen.',
|
||||
'utility.sticker.noSource': 'Anhang oder URL erforderlich.',
|
||||
'utility.sticker.added': 'Sticker **{name}** hinzugefügt.',
|
||||
'utility.embed.sent': 'Embed gesendet.',
|
||||
'utility.embed.invalidColor': 'Ungültige Farbe. Nutze z. B. 6366F1.',
|
||||
'utility.snipe.none': 'Nichts zum Anzeigen.',
|
||||
'utility.snipe.privacy':
|
||||
'Datenschutz: Snipe speichert gelöschte/bearbeitete Nachrichten kurzzeitig (1 h) in Redis.',
|
||||
'utility.snipe.deletedAt': 'Gelöschte Nachricht von {user}',
|
||||
'utility.snipe.editedAt': 'Bearbeitete Nachricht von {user}',
|
||||
'utility.translate.noContent': 'Kein übersetzbare Textinhalt.',
|
||||
'utility.translate.failed': 'Übersetzung fehlgeschlagen.',
|
||||
'utility.translate.result': '**Übersetzung:** {text}',
|
||||
'fun.8ball.result': '**Frage:** {question}\n**Antwort:** {answer}',
|
||||
'fun.dice.result': 'Würfel ({sides} Seiten): {rolls}',
|
||||
'fun.flip.result': 'Die Münze zeigt: **{result}**',
|
||||
'fun.rps.win': 'Du wähltest **{player}**, ich **{bot}**. Du gewinnst!',
|
||||
'fun.rps.lose': 'Du wähltest **{player}**, ich **{bot}**. Du verlierst!',
|
||||
'fun.rps.draw': 'Beide wählten **{player}**. Unentschieden!',
|
||||
'fun.rps.choice.rock': 'Stein',
|
||||
'fun.rps.choice.paper': 'Papier',
|
||||
'fun.rps.choice.scissors': 'Schere',
|
||||
'fun.choose.result': 'Ich wähle: **{choice}**',
|
||||
'fun.choose.tooFew': 'Gib mindestens zwei Optionen an (kommagetrennt).',
|
||||
'fun.trivia.title': 'Trivia',
|
||||
'fun.trivia.question': '**{question}**\n\n{options}',
|
||||
'fun.trivia.footer': 'Nur {user} kann antworten.',
|
||||
'fun.trivia.correct': '✅ Richtig!',
|
||||
'fun.trivia.incorrect': '❌ Falsch!',
|
||||
'fun.trivia.notOwner': 'Nur der Initiator kann diese Frage beantworten.',
|
||||
'fun.tictactoe.title': 'Tic-Tac-Toe',
|
||||
'fun.tictactoe.board': '❌: {x} | ⭕: {o}\n\n{board}',
|
||||
'fun.tictactoe.challenge': '{challenger} fordert {opponent} zu Tic-Tac-Toe heraus!',
|
||||
'fun.tictactoe.turn': 'Am Zug: {user}',
|
||||
'fun.tictactoe.won': '{user} gewinnt!',
|
||||
'fun.tictactoe.draw': 'Unentschieden!',
|
||||
'fun.tictactoe.invalidMove': 'Ungültiger Zug.',
|
||||
'fun.connect4.title': 'Connect 4',
|
||||
'fun.connect4.board': '🔴: {red} | 🟡: {yellow}\n\n{board}',
|
||||
'fun.connect4.challenge': '{challenger} fordert {opponent} zu Connect 4 heraus!',
|
||||
'fun.connect4.turn': 'Am Zug: {user}',
|
||||
'fun.connect4.won': '{user} gewinnt!',
|
||||
'fun.connect4.draw': 'Unentschieden!',
|
||||
'fun.connect4.invalidMove': 'Diese Spalte ist voll.',
|
||||
'fun.hangman.title': 'Hangman',
|
||||
'fun.hangman.progress': 'Wort: {word}\nFehler: {wrong}/{max}\nGeraten: {guessed}',
|
||||
'fun.hangman.won': 'Gewonnen! Das Wort war **{word}**.',
|
||||
'fun.hangman.lost': 'Verloren! Das Wort war **{word}**.',
|
||||
'fun.common.notParticipant': 'Du bist nicht an diesem Spiel beteiligt.',
|
||||
'fun.common.expired': 'Dieses Spiel ist abgelaufen oder existiert nicht mehr.',
|
||||
'fun.common.selfChallenge': 'Du kannst dich nicht selbst herausfordern.',
|
||||
'fun.common.botChallenge': 'Du kannst keinen Bot herausfordern.',
|
||||
'fun.media.disabled': 'Medien-Commands sind auf diesem Server deaktiviert.',
|
||||
'fun.media.fetchFailed': 'Bild konnte nicht geladen werden. Versuche es später erneut.',
|
||||
'fun.cat.title': 'Random Cat',
|
||||
'fun.dog.title': 'Random Dog'
|
||||
};
|
||||
const en: Dictionary = {
|
||||
'generic.noPermission': 'You do not have permission for this action.',
|
||||
@@ -148,7 +294,152 @@ const en: Dictionary = {
|
||||
'verification.error.generic': 'Verification failed.',
|
||||
'verification.captcha.success': 'Captcha passed. You have been verified.',
|
||||
'verification.captcha.invalid': 'Captcha invalid or expired.',
|
||||
'verification.captcha.title': 'Nexumi Verification'
|
||||
'verification.captcha.title': 'Nexumi Verification',
|
||||
'leveling.disabled': 'Leveling is disabled on this server.',
|
||||
'leveling.rank.title': 'Rank card',
|
||||
'leveling.leaderboard.title': 'Leaderboard',
|
||||
'leveling.leaderboard.empty': 'No XP data yet.',
|
||||
'leveling.leaderboard.entry': '{rank}. {user} — Level {level} ({xp} XP)',
|
||||
'leveling.leaderboard.footer': 'Top {count} members',
|
||||
'leveling.levelUp.default': 'Congratulations {user}, you reached level {level}!',
|
||||
'leveling.xp.give.success': 'Gave {amount} XP to {user}. New level: {level}.',
|
||||
'leveling.xp.remove.success': 'Removed {amount} XP from {user}. New level: {level}.',
|
||||
'leveling.xp.reset.success': 'Reset XP for {user}.',
|
||||
'leveling.xp.invalidAmount': 'Amount must be greater than 0.',
|
||||
'leveling.user.notFound': 'User not found on this server.',
|
||||
'economy.error.disabled': 'The economy module is disabled on this server.',
|
||||
'economy.error.insufficient': 'You do not have enough money.',
|
||||
'economy.error.cooldown': 'Cooldown active. Try again in {seconds} seconds.',
|
||||
'economy.error.invalid_amount': 'Invalid amount.',
|
||||
'economy.error.self_transfer': 'You cannot send money to yourself.',
|
||||
'economy.error.not_found': 'Entry not found.',
|
||||
'economy.error.out_of_stock': 'This item is out of stock.',
|
||||
'economy.error.role_assign_failed': 'Purchase succeeded, but the role could not be assigned.',
|
||||
'economy.error.game_in_progress': 'You already have an active blackjack game.',
|
||||
'economy.balance.result': 'Balance of {user}: {amount}',
|
||||
'economy.daily.success': 'Daily reward: {amount}. New balance: {balance}',
|
||||
'economy.weekly.success': 'Weekly reward: {amount}. New balance: {balance}',
|
||||
'economy.work.success': 'You earned {amount}. New balance: {balance}',
|
||||
'economy.pay.success': 'You sent {amount} to {user}. Your balance: {balance}',
|
||||
'economy.gamble.win': 'You won! +{amount}. Balance: {balance}',
|
||||
'economy.gamble.lose': 'You lost! -{amount}. Balance: {balance}',
|
||||
'economy.slots.win': '{reels}\nWinnings: {payout}. Balance: {balance}',
|
||||
'economy.slots.lose': '{reels}\nNo win. Balance: {balance}',
|
||||
'economy.coinflip.win': '{result} – you picked {choice}. You won! +{amount}. Balance: {balance}',
|
||||
'economy.coinflip.lose': '{result} – you picked {choice}. You lost! -{amount}. Balance: {balance}',
|
||||
'economy.choice.heads': 'Heads',
|
||||
'economy.choice.tails': 'Tails',
|
||||
'economy.shop.empty': 'The shop is empty.',
|
||||
'economy.shop.noDescription': 'No description',
|
||||
'economy.shop.itemLine': '**{name}** (`{id}`) – {price} | Stock: {stock}\n{description}',
|
||||
'economy.shop.purchased': 'You purchased **{name}**. Balance: {balance}',
|
||||
'economy.inventory.empty': 'Your inventory is empty.',
|
||||
'economy.inventory.itemLine': '- {name} × {quantity}',
|
||||
'economy.leaderboard.empty': 'No leaderboard entries yet.',
|
||||
'economy.leaderboard.line': '{rank}. {user} – {amount}',
|
||||
'economy.admin.give': 'Gave {amount} to {user}. New balance: {balance}',
|
||||
'economy.admin.remove': 'Removed {amount} from {user}. New balance: {balance}',
|
||||
'economy.admin.reset': 'Reset economy data for {user}.',
|
||||
'economy.blackjack.title': 'Blackjack',
|
||||
'economy.blackjack.hand': 'Bet: {bet}\n\n**You:** {playerHand} ({playerTotal})\n**Dealer:** {dealerHand} ({dealerTotal})',
|
||||
'economy.blackjack.button.hit': 'Hit',
|
||||
'economy.blackjack.button.stand': 'Stand',
|
||||
'economy.blackjack.outcome.win': 'You win {bet}!',
|
||||
'economy.blackjack.outcome.lose': 'You lose your bet.',
|
||||
'economy.blackjack.outcome.push': 'Push – bet returned.',
|
||||
'economy.blackjack.outcome.blackjack': 'Blackjack! You win {bet}!',
|
||||
'economy.blackjack.expired': 'This blackjack game expired or no longer exists.',
|
||||
'economy.blackjack.notOwner': 'Only the player can use these buttons.',
|
||||
'utility.error.invalid_when': 'Invalid time. Use e.g. 30m or a future ISO datetime.',
|
||||
'utility.error.invalid_datetime': 'Invalid date/time.',
|
||||
'utility.error.invalid_poll_options': 'Provide 2–5 comma-separated options.',
|
||||
'utility.error.channel_not_found': 'Channel not found.',
|
||||
'utility.error.role_not_found': 'Role not found.',
|
||||
'utility.error.poll_not_found': 'Poll not found.',
|
||||
'utility.error.reminder_channel_unavailable': 'Reminder channel unavailable.',
|
||||
'utility.poll.created': 'Poll created.',
|
||||
'utility.poll.closed': 'This poll is closed.',
|
||||
'utility.poll.notFound': 'Poll not found.',
|
||||
'utility.poll.invalidOption': 'Invalid option.',
|
||||
'utility.poll.alreadyVoted': 'You already voted.',
|
||||
'utility.poll.voted': 'Vote counted for **{option}**.',
|
||||
'utility.poll.votes': 'votes',
|
||||
'utility.poll.endsAt': 'Ends {time}',
|
||||
'utility.poll.multiSelect': 'Multi-select',
|
||||
'utility.poll.multiSelectHint': 'You may select multiple options.',
|
||||
'utility.remindme.created': 'Reminder created (ID: `{id}`).',
|
||||
'utility.reminders.none': 'No reminders.',
|
||||
'utility.reminders.line': '`{id}` — {time}: {content}',
|
||||
'utility.reminders.deleted': 'Reminder deleted.',
|
||||
'utility.reminders.notFound': 'Reminder not found.',
|
||||
'utility.afk.set': 'AFK set. Reason: {reason}',
|
||||
'utility.afk.welcomeBack': 'Welcome back! You were AFK: {reason}',
|
||||
'utility.afk.mention': '{user} is AFK: {reason}',
|
||||
'utility.avatar.title': 'Avatar of {user}',
|
||||
'utility.banner.title': 'Banner of {user}',
|
||||
'utility.banner.none': 'No banner set.',
|
||||
'utility.emoji.noSource': 'Attachment or URL required.',
|
||||
'utility.emoji.added': 'Added emoji {emoji}.',
|
||||
'utility.emoji.removed': 'Emoji removed.',
|
||||
'utility.emoji.notFound': 'Emoji not found.',
|
||||
'utility.emoji.messageNotFound': 'Message not found.',
|
||||
'utility.emoji.noEmojiInMessage': 'No custom emoji in message.',
|
||||
'utility.emoji.stolen': 'Stole emoji {emoji}.',
|
||||
'utility.sticker.noSource': 'Attachment or URL required.',
|
||||
'utility.sticker.added': 'Added sticker **{name}**.',
|
||||
'utility.embed.sent': 'Embed sent.',
|
||||
'utility.embed.invalidColor': 'Invalid color. Use e.g. 6366F1.',
|
||||
'utility.snipe.none': 'Nothing to show.',
|
||||
'utility.snipe.privacy':
|
||||
'Privacy: Snipe stores deleted/edited messages briefly (1 h) in Redis.',
|
||||
'utility.snipe.deletedAt': 'Deleted message by {user}',
|
||||
'utility.snipe.editedAt': 'Edited message by {user}',
|
||||
'utility.translate.noContent': 'No translatable text content.',
|
||||
'utility.translate.failed': 'Translation failed.',
|
||||
'utility.translate.result': '**Translation:** {text}',
|
||||
'fun.8ball.result': '**Question:** {question}\n**Answer:** {answer}',
|
||||
'fun.dice.result': 'Dice ({sides} sides): {rolls}',
|
||||
'fun.flip.result': 'The coin shows: **{result}**',
|
||||
'fun.rps.win': 'You picked **{player}**, I picked **{bot}**. You win!',
|
||||
'fun.rps.lose': 'You picked **{player}**, I picked **{bot}**. You lose!',
|
||||
'fun.rps.draw': 'Both picked **{player}**. Draw!',
|
||||
'fun.rps.choice.rock': 'Rock',
|
||||
'fun.rps.choice.paper': 'Paper',
|
||||
'fun.rps.choice.scissors': 'Scissors',
|
||||
'fun.choose.result': 'I choose: **{choice}**',
|
||||
'fun.choose.tooFew': 'Provide at least two comma-separated options.',
|
||||
'fun.trivia.title': 'Trivia',
|
||||
'fun.trivia.question': '**{question}**\n\n{options}',
|
||||
'fun.trivia.footer': 'Only {user} can answer.',
|
||||
'fun.trivia.correct': '✅ Correct!',
|
||||
'fun.trivia.incorrect': '❌ Wrong!',
|
||||
'fun.trivia.notOwner': 'Only the initiator can answer this question.',
|
||||
'fun.tictactoe.title': 'Tic-Tac-Toe',
|
||||
'fun.tictactoe.board': '❌: {x} | ⭕: {o}\n\n{board}',
|
||||
'fun.tictactoe.challenge': '{challenger} challenges {opponent} to Tic-Tac-Toe!',
|
||||
'fun.tictactoe.turn': 'Turn: {user}',
|
||||
'fun.tictactoe.won': '{user} wins!',
|
||||
'fun.tictactoe.draw': 'Draw!',
|
||||
'fun.tictactoe.invalidMove': 'Invalid move.',
|
||||
'fun.connect4.title': 'Connect 4',
|
||||
'fun.connect4.board': '🔴: {red} | 🟡: {yellow}\n\n{board}',
|
||||
'fun.connect4.challenge': '{challenger} challenges {opponent} to Connect 4!',
|
||||
'fun.connect4.turn': 'Turn: {user}',
|
||||
'fun.connect4.won': '{user} wins!',
|
||||
'fun.connect4.draw': 'Draw!',
|
||||
'fun.connect4.invalidMove': 'This column is full.',
|
||||
'fun.hangman.title': 'Hangman',
|
||||
'fun.hangman.progress': 'Word: {word}\nWrong: {wrong}/{max}\nGuessed: {guessed}',
|
||||
'fun.hangman.won': 'You won! The word was **{word}**.',
|
||||
'fun.hangman.lost': 'You lost! The word was **{word}**.',
|
||||
'fun.common.notParticipant': 'You are not a participant in this game.',
|
||||
'fun.common.expired': 'This game expired or no longer exists.',
|
||||
'fun.common.selfChallenge': 'You cannot challenge yourself.',
|
||||
'fun.common.botChallenge': 'You cannot challenge a bot.',
|
||||
'fun.media.disabled': 'Media commands are disabled on this server.',
|
||||
'fun.media.fetchFailed': 'Could not load image. Try again later.',
|
||||
'fun.cat.title': 'Random Cat',
|
||||
'fun.dog.title': 'Random Dog'
|
||||
};
|
||||
|
||||
const locales: Record<Locale, Dictionary> = { de, en };
|
||||
|
||||
112
packages/shared/src/phase3.ts
Normal file
112
packages/shared/src/phase3.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
/** XP required to go from level L to L+1. */
|
||||
export function xpForNextLevel(level: number): number {
|
||||
return 5 * level * level + 50 * level + 100;
|
||||
}
|
||||
|
||||
/** Total XP needed to reach a given level from 0. */
|
||||
export function totalXpForLevel(level: number): number {
|
||||
let total = 0;
|
||||
for (let i = 0; i < level; i += 1) {
|
||||
total += xpForNextLevel(i);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
/** Derive level from cumulative XP. */
|
||||
export function levelFromXp(xp: number): number {
|
||||
let level = 0;
|
||||
let remaining = Math.max(0, xp);
|
||||
while (remaining >= xpForNextLevel(level)) {
|
||||
remaining -= xpForNextLevel(level);
|
||||
level += 1;
|
||||
if (level > 1000) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return level;
|
||||
}
|
||||
|
||||
export function progressInLevel(xp: number): { level: number; intoLevel: number; needed: number } {
|
||||
const level = levelFromXp(xp);
|
||||
const base = totalXpForLevel(level);
|
||||
const intoLevel = xp - base;
|
||||
const needed = xpForNextLevel(level);
|
||||
return { level, intoLevel, needed };
|
||||
}
|
||||
|
||||
export function randomInt(min: number, max: number): number {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
export const LevelUpModeSchema = z.enum(['CHANNEL', 'DM', 'OFF']);
|
||||
export type LevelUpMode = z.infer<typeof LevelUpModeSchema>;
|
||||
|
||||
export const MultiplierMapSchema = z.record(z.string(), z.number().positive());
|
||||
export type MultiplierMap = z.infer<typeof MultiplierMapSchema>;
|
||||
|
||||
export function parseMultiplierMap(raw: unknown): MultiplierMap {
|
||||
const parsed = MultiplierMapSchema.safeParse(raw);
|
||||
return parsed.success ? parsed.data : {};
|
||||
}
|
||||
|
||||
export function formatCurrency(amount: number, symbol: string, name: string): string {
|
||||
return `${symbol} ${amount.toLocaleString()} ${name}`;
|
||||
}
|
||||
|
||||
export const EIGHT_BALL_ANSWERS = {
|
||||
de: [
|
||||
'Ja.',
|
||||
'Nein.',
|
||||
'Vielleicht.',
|
||||
'Frag später nochmal.',
|
||||
'Sieht gut aus.',
|
||||
'Zweifelhaft.',
|
||||
'Auf keinen Fall.',
|
||||
'Absolut.'
|
||||
],
|
||||
en: [
|
||||
'Yes.',
|
||||
'No.',
|
||||
'Maybe.',
|
||||
'Ask again later.',
|
||||
'Looks good.',
|
||||
'Doubtful.',
|
||||
'Absolutely not.',
|
||||
'Absolutely.'
|
||||
]
|
||||
} as const;
|
||||
|
||||
export const TRIVIA_QUESTIONS = [
|
||||
{
|
||||
q: { de: 'Welche Farbe hat der Himmel an einem klaren Tag?', en: 'What color is the sky on a clear day?' },
|
||||
options: { de: ['Blau', 'Grün', 'Rot', 'Gelb'], en: ['Blue', 'Green', 'Red', 'Yellow'] },
|
||||
answer: 0
|
||||
},
|
||||
{
|
||||
q: { de: 'Wie viele Kontinente gibt es?', en: 'How many continents are there?' },
|
||||
options: { de: ['5', '6', '7', '8'], en: ['5', '6', '7', '8'] },
|
||||
answer: 2
|
||||
},
|
||||
{
|
||||
q: { de: 'Welches Tier ist ein Säugetier?', en: 'Which animal is a mammal?' },
|
||||
options: { de: ['Hai', 'Delphin', 'Thunfisch', 'Lachs'], en: ['Shark', 'Dolphin', 'Tuna', 'Salmon'] },
|
||||
answer: 1
|
||||
},
|
||||
{
|
||||
q: { de: 'Was ist 2 + 2?', en: 'What is 2 + 2?' },
|
||||
options: { de: ['3', '4', '5', '22'], en: ['3', '4', '5', '22'] },
|
||||
answer: 1
|
||||
},
|
||||
{
|
||||
q: { de: 'Hauptstadt von Frankreich?', en: 'Capital of France?' },
|
||||
options: { de: ['Berlin', 'Madrid', 'Paris', 'Rom'], en: ['Berlin', 'Madrid', 'Paris', 'Rome'] },
|
||||
answer: 2
|
||||
}
|
||||
] as const;
|
||||
|
||||
export const HANGMAN_WORDS = {
|
||||
de: ['DISCORD', 'NEXUMI', 'SERVER', 'MODERATION', 'BOT', 'KANAL', 'ROLLE'],
|
||||
en: ['DISCORD', 'NEXUMI', 'SERVER', 'MODERATION', 'BOT', 'CHANNEL', 'ROLE']
|
||||
} as const;
|
||||
Reference in New Issue
Block a user