diff --git a/locales/de.json b/locales/de.json index 320d9b3e..a6b0f706 100644 --- a/locales/de.json +++ b/locales/de.json @@ -256,7 +256,27 @@ "edit-xp-value-description": "Neue Erfahrungspunktemenge des Nutzers", "edit-xp-description": "Betrüge deine Community und bearbeite die Erfahrungspunkte eines Nutzers", "random-messages-enabled-but-non-configured": "Zufällige Nachrichten sind aktiviert, allerdings wurden keine zufälligen Nachrichten festgelegt. Ignoriere Anweisung.", - "granted-rewards-audit-log": "Rollen aktualisiert um sicherzustellen, dass der/die Nutzer:in die benötigten Rollen hat" + "granted-rewards-audit-log": "Rollen aktualisiert um sicherzustellen, dass der/die Nutzer:in die benötigten Rollen hat", + "rewards-command-description": "Level-Belohnungen verwalten", + "rewards-add-description": "Rollen zu einer Level-Belohnung hinzufügen", + "rewards-set-description": "Rollen für eine Level-Belohnung setzen", + "rewards-remove-description": "Eine Rolle aus einer Level-Belohnung entfernen", + "rewards-clear-description": "Alle Belohnungen für ein Level entfernen", + "rewards-list-description": "Konfigurierte Level-Belohnungen anzeigen", + "rewards-level-description": "Level zum Konfigurieren", + "rewards-role-description": "Rolle, die vergeben wird", + "rewards-replace-description": "Ersetzt vorherige ersetzbare Belohnungen", + "rewards-replace-on": "ersetzbar", + "rewards-replace-off": "behalten", + "rewards-none": "keine", + "rewards-added": "Level %l Belohnungen: %roles (%replace)", + "rewards-set": "Level %l Belohnungen gesetzt: %roles (%replace)", + "rewards-removed": "%role aus Level %l Belohnungen entfernt", + "rewards-cleared": "Belohnungen für Level %l entfernt", + "rewards-level-not-found": "Keine Belohnungen für Level %l konfiguriert", + "rewards-list-empty": "Noch keine Level-Belohnungen konfiguriert", + "rewards-list-one": "Level %l: %roles (%replace)", + "rewards-list-line": "Level %l: %roles (%replace)" }, "partner-list": { "could-not-give-role": "%u konnte keine Rolle gegeben werden", diff --git a/locales/en.json b/locales/en.json index 4cf4437e..2f89cbc0 100644 --- a/locales/en.json +++ b/locales/en.json @@ -245,7 +245,27 @@ "role-factors-total": "Multiplied together, the user receives **%f times more XP** for every message.", "edit-level-description": "Betrays your community and edits a user's levels", "random-messages-enabled-but-non-configured": "You have random messages enabled, but have non configured. Ignoring config.randomMessages configuration.", - "granted-rewards-audit-log": "Updated roles to make sure, they have the level role they need" + "granted-rewards-audit-log": "Updated roles to make sure, they have the level role they need", + "rewards-command-description": "Manage level reward roles", + "rewards-add-description": "Add roles to a level reward", + "rewards-set-description": "Set roles for a level reward", + "rewards-remove-description": "Remove a role from a level reward", + "rewards-clear-description": "Remove all rewards for a level", + "rewards-list-description": "List configured level rewards", + "rewards-level-description": "Level to configure", + "rewards-role-description": "Role to grant", + "rewards-replace-description": "Replace previous replaceable rewards", + "rewards-replace-on": "replaceable", + "rewards-replace-off": "kept", + "rewards-none": "none", + "rewards-added": "Level %l rewards: %roles (%replace)", + "rewards-set": "Level %l rewards set to: %roles (%replace)", + "rewards-removed": "Removed %role from level %l rewards", + "rewards-cleared": "Cleared rewards for level %l", + "rewards-level-not-found": "No rewards configured for level %l", + "rewards-list-empty": "No level rewards configured yet", + "rewards-list-one": "Level %l: %roles (%replace)", + "rewards-list-line": "Level %l: %roles (%replace)" }, "team-list": { "channel-not-found": "Could not find channel with ID %c or the channel has a wrong type (only text-channels supported)", @@ -1018,4 +1038,4 @@ "owner-cannot-be-renamed": "The owner of the server (%u) cannot be renamed.", "nickname-error": "An error occurred while trying to change the nickname of %u: %e" } -} \ No newline at end of file +} diff --git a/modules/levels/commands/manage-levels.js b/modules/levels/commands/manage-levels.js index b58d69b5..8a5af1ae 100644 --- a/modules/levels/commands/manage-levels.js +++ b/modules/levels/commands/manage-levels.js @@ -1,6 +1,60 @@ +const fs = require('fs'); +const path = require('path'); +const jsonfile = require('jsonfile'); const {registerNeededEdit} = require('../leaderboardChannel'); const {localize} = require('../../../src/functions/localize'); const {formatDiscordUserName} = require('../../../src/functions/helpers'); +const {reloadConfig} = require('../../../src/functions/configuration'); +const {getReplaceableRewardRoleIds, getRewardForLevel} = require('../rewards'); + +function getRewardsConfigPath(client) { + return path.join(client.configDir, 'levels', 'reward-roles.json'); +} + +function ensureRewardsDir(client) { + const dir = path.join(client.configDir, 'levels'); + if (!fs.existsSync(dir)) fs.mkdirSync(dir, {recursive: true}); +} + +function readRewards(client) { + const filePath = getRewardsConfigPath(client); + try { + const data = jsonfile.readFileSync(filePath); + return Array.isArray(data) ? data : []; + } catch { + return []; + } +} + +function writeRewards(client, rewards) { + ensureRewardsDir(client); + jsonfile.writeFileSync(getRewardsConfigPath(client), rewards, {spaces: 2}); +} + +function collectRoles(interaction) { + const roles = [ + interaction.options.getRole('role', true), + interaction.options.getRole('role2'), + interaction.options.getRole('role3'), + interaction.options.getRole('role4'), + interaction.options.getRole('role5') + ].filter(Boolean).map(r => r.id); + return [...new Set(roles)]; +} + +function formatRoles(roleIds) { + if (!roleIds || roleIds.length === 0) return localize('levels', 'rewards-none'); + return roleIds.map(id => `<@&${id}>`).join(', '); +} + +function findEntry(rewards, level) { + return rewards.find(r => parseInt(r.level) === level); +} + +async function saveAndReload(interaction, rewards) { + writeRewards(interaction.client, rewards); + await reloadConfig(interaction.client); +} async function runXPAction(interaction, newXP) { const member = interaction.options.getMember('user'); @@ -26,13 +80,14 @@ async function runXPAction(interaction, newXP) { const nextLevelXp = user.level * 750 + ((user.level - 1) * 500); if (nextLevelXp <= user.xp) { user.level = user.level + 1; - if (interaction.client.configurations.levels.config.reward_roles[user.level.toString()]) { - if (interaction.client.configurations.levels.config.reward_roles[user.level.toString()]) { - for (const role of Object.values(interaction.client.configurations.levels.config.reward_roles)) { + const rewardConfig = getRewardForLevel(interaction.client, user.level); + if (rewardConfig) { + if (rewardConfig.replacePrevious) { + for (const role of getReplaceableRewardRoleIds(interaction.client)) { if (member.roles.cache.has(role)) member.roles.remove(role, '[levels] ' + localize('levels', 'granted-rewards-audit-log')).catch(); } } - member.roles.add(interaction.client.configurations.levels.config.reward_roles[user.level.toString()]); + member.roles.add(rewardConfig.roles); } runXPCheck(); } @@ -81,13 +136,14 @@ async function runLevelAction(interaction, newLevel) { content: '⚠️ ' + localize('levels', 'negative-level') }); user.xp = (user.level - 1) * 750 + ((user.level - 2) * 500); - if (interaction.client.configurations.levels.config.reward_roles[user.level.toString()]) { - if (interaction.client.configurations.levels.config.reward_roles[user.level.toString()]) { - for (const role of Object.values(interaction.client.configurations.levels.config.reward_roles)) { + const rewardConfig = getRewardForLevel(interaction.client, user.level); + if (rewardConfig) { + if (rewardConfig.replacePrevious) { + for (const role of getReplaceableRewardRoleIds(interaction.client)) { if (member.roles.cache.has(role)) member.roles.remove(role, '[levels] ' + localize('levels', 'granted-rewards-audit-log')).catch(); } } - member.roles.add(interaction.client.configurations.levels.config.reward_roles[user.level.toString()]); + member.roles.add(rewardConfig.roles); } @@ -115,6 +171,123 @@ async function runLevelAction(interaction, newLevel) { } module.exports.subcommands = { + 'rewards': { + 'add': async function (interaction) { + const level = interaction.options.getInteger('level', true); + const roles = collectRoles(interaction); + const replacePrevious = interaction.options.getBoolean('replaceprevious'); + + const rewards = readRewards(interaction.client); + let entry = findEntry(rewards, level); + if (!entry) { + entry = {level, roles: [], replacePrevious: false}; + rewards.push(entry); + } + entry.roles = [...new Set([...(entry.roles || []), ...roles])]; + if (typeof replacePrevious === 'boolean') entry.replacePrevious = replacePrevious; + + await saveAndReload(interaction, rewards); + return interaction.reply({ + ephemeral: true, + content: localize('levels', 'rewards-added', { + l: level, + roles: formatRoles(entry.roles), + replace: entry.replacePrevious ? localize('levels', 'rewards-replace-on') : localize('levels', 'rewards-replace-off') + }) + }); + }, + 'set': async function (interaction) { + const level = interaction.options.getInteger('level', true); + const roles = collectRoles(interaction); + const replacePrevious = interaction.options.getBoolean('replaceprevious'); + + const rewards = readRewards(interaction.client); + let entry = findEntry(rewards, level); + if (!entry) { + entry = {level, roles: [], replacePrevious: false}; + rewards.push(entry); + } + entry.roles = roles; + if (typeof replacePrevious === 'boolean') entry.replacePrevious = replacePrevious; + + await saveAndReload(interaction, rewards); + return interaction.reply({ + ephemeral: true, + content: localize('levels', 'rewards-set', { + l: level, + roles: formatRoles(entry.roles), + replace: entry.replacePrevious ? localize('levels', 'rewards-replace-on') : localize('levels', 'rewards-replace-off') + }) + }); + }, + 'remove': async function (interaction) { + const level = interaction.options.getInteger('level', true); + const role = interaction.options.getRole('role', true); + + const rewards = readRewards(interaction.client); + const entry = findEntry(rewards, level); + if (!entry) { + return interaction.reply({ephemeral: true, content: localize('levels', 'rewards-level-not-found', {l: level})}); + } + entry.roles = (entry.roles || []).filter(r => r !== role.id); + if (entry.roles.length === 0) { + const idx = rewards.indexOf(entry); + if (idx >= 0) rewards.splice(idx, 1); + } + + await saveAndReload(interaction, rewards); + return interaction.reply({ + ephemeral: true, + content: localize('levels', 'rewards-removed', { + l: level, + role: role.toString() + }) + }); + }, + 'clear': async function (interaction) { + const level = interaction.options.getInteger('level', true); + const rewards = readRewards(interaction.client); + const before = rewards.length; + const filtered = rewards.filter(r => parseInt(r.level) !== level); + if (filtered.length === before) { + return interaction.reply({ephemeral: true, content: localize('levels', 'rewards-level-not-found', {l: level})}); + } + await saveAndReload(interaction, filtered); + return interaction.reply({ephemeral: true, content: localize('levels', 'rewards-cleared', {l: level})}); + }, + 'list': async function (interaction) { + const level = interaction.options.getInteger('level'); + const rewards = readRewards(interaction.client); + + if (level) { + const entry = findEntry(rewards, level); + if (!entry) { + return interaction.reply({ephemeral: true, content: localize('levels', 'rewards-level-not-found', {l: level})}); + } + return interaction.reply({ + ephemeral: true, + content: localize('levels', 'rewards-list-one', { + l: level, + roles: formatRoles(entry.roles || []), + replace: entry.replacePrevious ? localize('levels', 'rewards-replace-on') : localize('levels', 'rewards-replace-off') + }) + }); + } + + if (rewards.length === 0) { + return interaction.reply({ephemeral: true, content: localize('levels', 'rewards-list-empty')}); + } + const lines = rewards + .slice() + .sort((a, b) => parseInt(a.level) - parseInt(b.level)) + .map(r => localize('levels', 'rewards-list-line', { + l: r.level, + roles: formatRoles(r.roles || []), + replace: r.replacePrevious ? localize('levels', 'rewards-replace-on') : localize('levels', 'rewards-replace-off') + })); + return interaction.reply({ephemeral: true, content: lines.join('\n')}); + } + }, 'reset-xp': async function (interaction) { const type = interaction.options.getUser('user') ? 'user' : 'server'; if (!interaction.options.getBoolean('confirm')) return interaction.reply({ @@ -198,6 +371,155 @@ module.exports.config = { options: function (client) { const array = [{ + type: 'SUB_COMMAND_GROUP', + name: 'rewards', + description: localize('levels', 'rewards-command-description'), + options: [ + { + type: 'SUB_COMMAND', + name: 'add', + description: localize('levels', 'rewards-add-description'), + options: [ + { + type: 'INTEGER', + required: true, + name: 'level', + description: localize('levels', 'rewards-level-description') + }, + { + type: 'ROLE', + required: true, + name: 'role', + description: localize('levels', 'rewards-role-description') + }, + { + type: 'ROLE', + required: false, + name: 'role2', + description: localize('levels', 'rewards-role-description') + }, + { + type: 'ROLE', + required: false, + name: 'role3', + description: localize('levels', 'rewards-role-description') + }, + { + type: 'ROLE', + required: false, + name: 'role4', + description: localize('levels', 'rewards-role-description') + }, + { + type: 'ROLE', + required: false, + name: 'role5', + description: localize('levels', 'rewards-role-description') + }, + { + type: 'BOOLEAN', + required: false, + name: 'replaceprevious', + description: localize('levels', 'rewards-replace-description') + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'set', + description: localize('levels', 'rewards-set-description'), + options: [ + { + type: 'INTEGER', + required: true, + name: 'level', + description: localize('levels', 'rewards-level-description') + }, + { + type: 'ROLE', + required: true, + name: 'role', + description: localize('levels', 'rewards-role-description') + }, + { + type: 'ROLE', + required: false, + name: 'role2', + description: localize('levels', 'rewards-role-description') + }, + { + type: 'ROLE', + required: false, + name: 'role3', + description: localize('levels', 'rewards-role-description') + }, + { + type: 'ROLE', + required: false, + name: 'role4', + description: localize('levels', 'rewards-role-description') + }, + { + type: 'ROLE', + required: false, + name: 'role5', + description: localize('levels', 'rewards-role-description') + }, + { + type: 'BOOLEAN', + required: false, + name: 'replaceprevious', + description: localize('levels', 'rewards-replace-description') + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'remove', + description: localize('levels', 'rewards-remove-description'), + options: [ + { + type: 'INTEGER', + required: true, + name: 'level', + description: localize('levels', 'rewards-level-description') + }, + { + type: 'ROLE', + required: true, + name: 'role', + description: localize('levels', 'rewards-role-description') + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'clear', + description: localize('levels', 'rewards-clear-description'), + options: [ + { + type: 'INTEGER', + required: true, + name: 'level', + description: localize('levels', 'rewards-level-description') + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'list', + description: localize('levels', 'rewards-list-description'), + options: [ + { + type: 'INTEGER', + required: false, + name: 'level', + description: localize('levels', 'rewards-level-description') + } + ] + } + ] + }, { type: 'SUB_COMMAND', name: 'reset-xp', description: localize('levels', 'reset-xp-description'), @@ -349,4 +671,4 @@ module.exports.config = { } return array; } -}; \ No newline at end of file +}; diff --git a/modules/levels/configs/reward-roles.json b/modules/levels/configs/reward-roles.json new file mode 100644 index 00000000..594ae7c3 --- /dev/null +++ b/modules/levels/configs/reward-roles.json @@ -0,0 +1,60 @@ +{ + "description": { + "en": "Configure reward roles per level", + "de": "Belohnungsrollen pro Level konfigurieren" + }, + "humanName": { + "en": "Reward roles", + "de": "Belohnungsrollen" + }, + "filename": "reward-roles.json", + "configElements": true, + "content": [ + { + "name": "level", + "humanName": { + "en": "Level", + "de": "Level" + }, + "default": { + "en": "" + }, + "description": { + "en": "Level at which the reward should be granted", + "de": "Level, bei dem die Belohnung vergeben wird" + }, + "type": "integer" + }, + { + "name": "roles", + "humanName": { + "en": "Reward roles", + "de": "Belohnungsrollen" + }, + "default": { + "en": [] + }, + "description": { + "en": "Roles that should be granted at this level", + "de": "Rollen, die bei diesem Level vergeben werden" + }, + "type": "array", + "content": "roleID" + }, + { + "name": "replacePrevious", + "humanName": { + "en": "Replace previous reward roles", + "de": "Vorherige Belohnungsrollen ersetzen" + }, + "default": { + "en": false + }, + "description": { + "en": "If enabled, previous reward roles will be removed when this reward is granted", + "de": "Wenn aktiviert, werden vorherige Belohnungsrollen entfernt" + }, + "type": "boolean" + } + ] +} diff --git a/modules/levels/events/messageCreate.js b/modules/levels/events/messageCreate.js index c9cd2803..46a34b52 100644 --- a/modules/levels/events/messageCreate.js +++ b/modules/levels/events/messageCreate.js @@ -6,6 +6,7 @@ const { } = require('../../../src/functions/helpers'); const {registerNeededEdit} = require('../leaderboardChannel'); const {localize} = require('../../../src/functions/localize'); +const {getReplaceableRewardRoleIds, getRewardForLevel} = require('../rewards'); const cooldown = new Set(); let currentlyLevelingUp = []; @@ -57,7 +58,8 @@ module.exports.run = async (client, msg) => { const channel = client.channels.cache.find(c => c.id === moduleConfig.level_up_channel_id); const specialMessage = client.configurations['levels']['special-levelup-messages'].find(m => m.level === user.level); - const isRewardMessage = !!moduleConfig.reward_roles[user.level.toString()]; + const rewardConfig = getRewardForLevel(client, user.level); + const isRewardMessage = !!rewardConfig; const randomMessages = client.configurations['levels']['random-levelup-messages'].filter(m => m.type === (isRewardMessage ? 'with-reward' : 'normal')); let messageToSend = moduleStrings.level_up_message; @@ -68,13 +70,15 @@ module.exports.run = async (client, msg) => { else if (randomMessages.length !== 0) messageToSend = randomElementFromArray(randomMessages).message; } - if (isRewardMessage) { - if (moduleConfig.onlyTopLevelRole) { - for (const role of Object.values(moduleConfig.reward_roles)) { - if (msg.member.roles.cache.has(role)) await msg.member.roles.remove(role, '[levels] ' + localize('levels', 'granted-rewards-audit-log')).catch(); + if (rewardConfig) { + if (rewardConfig.replacePrevious) { + for (const role of getReplaceableRewardRoleIds(client)) { + if (msg.member.roles.cache.has(role)) { + await msg.member.roles.remove(role, '[levels] ' + localize('levels', 'granted-rewards-audit-log')).catch(); + } } } - await msg.member.roles.add(moduleConfig.reward_roles[user.level.toString()], '[levels]' + localize('levels', 'granted-rewards-audit-log')).catch(); + await msg.member.roles.add(rewardConfig.roles, '[levels]' + localize('levels', 'granted-rewards-audit-log')).catch(); } if (specialMessage) messageToSend = specialMessage.message; @@ -83,7 +87,7 @@ module.exports.run = async (client, msg) => { '%avatarURL%': msg.author.avatarURL() || msg.author.defaultAvatarURL, '%username%': msg.author.username, '%newLevel%': user.level, - '%role%': isRewardMessage ? `<@&${moduleConfig.reward_roles[user.level.toString()]}>` : localize('levels', 'no-role'), + '%role%': rewardConfig ? rewardConfig.roles.map(r => `<@&${r}>`).join(', ') : localize('levels', 'no-role'), '%tag%': formatDiscordUserName(msg.author) }, {allowedMentions: {parse: ['users']}})); currentlyLevelingUp = currentlyLevelingUp.filter(f => f !== msg.author.id); @@ -105,4 +109,4 @@ module.exports.run = async (client, msg) => { cooldown.delete(msg.author.id); }, moduleConfig.cooldown); await user.save(); -}; \ No newline at end of file +}; diff --git a/modules/levels/module.json b/modules/levels/module.json index 4fbb00c6..ca46531b 100644 --- a/modules/levels/module.json +++ b/modules/levels/module.json @@ -14,6 +14,7 @@ "models-dir": "/models", "config-example-files": [ "configs/config.json", + "configs/reward-roles.json", "configs/strings.json", "configs/random-levelup-messages.json", "configs/special-levelup-messages.json" @@ -25,4 +26,4 @@ "en": "Easy to use levelsystem with a lot of customization!", "de": "Einfaches Level-System mit vielen Anpassungsmöglichkeiten!" } -} \ No newline at end of file +} diff --git a/modules/levels/rewards.js b/modules/levels/rewards.js new file mode 100644 index 00000000..714be139 --- /dev/null +++ b/modules/levels/rewards.js @@ -0,0 +1,45 @@ +function getRewardEntries(client) { + const rewardEntries = client.configurations?.levels?.['reward-roles']; + return Array.isArray(rewardEntries) ? rewardEntries : []; +} + +function getReplaceableRewardRoleIds(client) { + const moduleConfig = client.configurations['levels']['config']; + const rewardEntries = getRewardEntries(client); + const roles = new Set(); + if (rewardEntries.length !== 0) { + for (const entry of rewardEntries) { + if (!entry.replacePrevious) continue; + if (!Array.isArray(entry.roles)) continue; + for (const roleId of entry.roles) roles.add(roleId); + } + } else if (moduleConfig.reward_roles) { + for (const roleId of Object.values(moduleConfig.reward_roles)) roles.add(roleId); + } + return [...roles]; +} + +function getRewardForLevel(client, level) { + const moduleConfig = client.configurations['levels']['config']; + const rewardEntries = getRewardEntries(client); + const entry = rewardEntries.find(r => parseInt(r.level) === level); + if (entry) { + const roles = Array.isArray(entry.roles) ? entry.roles.filter(Boolean) : []; + if (roles.length === 0) return null; + return { + roles, + replacePrevious: !!entry.replacePrevious + }; + } + const legacyRole = moduleConfig.reward_roles ? moduleConfig.reward_roles[level.toString()] : null; + if (!legacyRole) return null; + return { + roles: [legacyRole], + replacePrevious: !!moduleConfig.onlyTopLevelRole + }; +} + +module.exports = { + getReplaceableRewardRoleIds, + getRewardForLevel +};