-
-
Notifications
You must be signed in to change notification settings - Fork 48
reward system for level system #175
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Adds a new reward-role configuration model for the levels module (multi-role rewards per level with optional “replace previous” behavior), and wires it into the level-up flow (message-based leveling and cheat-based level/xp edits). Also introduces a new config example file and localization keys intended for reward-management commands.
Changes:
- Add
modules/levels/rewards.jshelper to resolve per-level rewards and compute replaceable role IDs (with legacy fallback toreward_roles/onlyTopLevelRole). - Update role-granting logic in
messageCreateandmanage-levelsto use the new reward resolution. - Add
configs/reward-roles.jsonconfig schema and extend locales with reward-management strings.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| modules/levels/rewards.js | New reward resolution + replaceable-role computation with legacy fallback |
| modules/levels/module.json | Registers configs/reward-roles.json as a module config example file |
| modules/levels/events/messageCreate.js | Uses new reward resolution when granting/removing roles on level-up |
| modules/levels/configs/reward-roles.json | New config-element schema for per-level rewards (multi-role + replace flag) |
| modules/levels/commands/manage-levels.js | Updates cheat flows to apply reward roles via new reward helpers |
| locales/en.json | Adds new reward-management localization keys |
| locales/de.json | Adds new reward-management localization keys |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| 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); | ||
| } |
Copilot
AI
Feb 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
member.roles.add(rewardConfig.roles) returns a Promise but isn’t awaited or caught. If adding the role(s) fails (missing permissions/unknown role), this can surface as an unhandled rejection. Please await it and/or attach .catch() (and consider passing an audit-log reason for consistency with message-based leveling).
| } | ||
| } | ||
| member.roles.add(interaction.client.configurations.levels.config.reward_roles[user.level.toString()]); | ||
| member.roles.add(rewardConfig.roles); |
Copilot
AI
Feb 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
member.roles.add(rewardConfig.roles) returns a Promise but isn’t awaited or caught. If adding the role(s) fails (missing permissions/unknown role), this can surface as an unhandled rejection. Please await it and/or attach .catch() (and consider passing an audit-log reason for consistency with message-based leveling).
| member.roles.add(rewardConfig.roles); | |
| await member.roles.add(rewardConfig.roles, '[levels] ' + localize('levels', 'granted-rewards-audit-log')).catch(); |
| const {registerNeededEdit} = require('../leaderboardChannel'); | ||
| const {localize} = require('../../../src/functions/localize'); | ||
| const {formatDiscordUserName} = require('../../../src/functions/helpers'); | ||
| const {getReplaceableRewardRoleIds, getRewardForLevel} = require('../rewards'); | ||
|
|
Copilot
AI
Feb 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PR description mentions adding /manage-levels rewards ... subcommands, and new locale keys for reward-management commands were added, but manage-levels.js currently only defines reset-xp, edit-xp, and edit-level options (no rewards subcommand group). Either implement and register the rewards subcommands in this PR, or update the PR description/remove unused locale keys to avoid drifting docs/UI text.
| 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); | ||
| } |
Copilot
AI
Feb 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getReplaceableRewardRoleIds/getRewardForLevel treat an empty levels['reward-roles'] array the same as “not configured” and fall back to legacy config.reward_roles. Because config-element files are always loaded as an array (often [] by default), there’s no way to intentionally disable rewards via the new config if legacy reward_roles is still populated (e.g., after using a future “clear” command). Consider distinguishing “missing/undefined” from “configured but empty” (e.g., have getRewardEntries return null when the config key is absent/not an array, and only fall back to legacy in that case), or add an explicit flag controlling legacy fallback.
| function getRewardForLevel(client, level) { | ||
| const moduleConfig = client.configurations['levels']['config']; | ||
| const rewardEntries = getRewardEntries(client); | ||
| const entry = rewardEntries.find(r => parseInt(r.level) === level); |
Copilot
AI
Feb 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Level lookup uses parseInt(r.level) === level, which can match unexpected strings like "10abc" and omits an explicit radix. Prefer strict numeric coercion/validation (e.g., Number(r.level) with Number.isInteger) before comparing to level.
| const entry = rewardEntries.find(r => parseInt(r.level) === level); | |
| const entry = rewardEntries.find(r => { | |
| const rLevel = Number(r.level); | |
| return Number.isInteger(rLevel) && rLevel === level; | |
| }); |
| 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]; |
Copilot
AI
Feb 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replacement logic builds a flat set of “replaceable role IDs” across all entries with replacePrevious: true. If the same role ID is ever used in both a replaceable reward and a non-replaceable (“kept”) reward, leveling into a replaceable reward will remove that role even if the member has it due to the kept reward. Consider enforcing uniqueness of role IDs across rewards, or tracking replaceability per (level, role) instead of per role ID.
| 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]; | |
| // Track roles that are ever granted with replacePrevious = true vs false | |
| const replaceableRoles = new Set(); | |
| const keptRoles = new Set(); | |
| if (rewardEntries.length !== 0) { | |
| for (const entry of rewardEntries) { | |
| if (!Array.isArray(entry.roles)) continue; | |
| if (entry.replacePrevious) { | |
| for (const roleId of entry.roles) { | |
| if (roleId) replaceableRoles.add(roleId); | |
| } | |
| } else { | |
| for (const roleId of entry.roles) { | |
| if (roleId) keptRoles.add(roleId); | |
| } | |
| } | |
| } | |
| } else if (moduleConfig.reward_roles && moduleConfig.onlyTopLevelRole) { | |
| // Legacy configuration: only treat roles as replaceable when legacy | |
| // behavior (onlyTopLevelRole) indicates previous roles should be replaced. | |
| for (const roleId of Object.values(moduleConfig.reward_roles)) { | |
| if (roleId) replaceableRoles.add(roleId); | |
| } | |
| } | |
| // A role is considered replaceable only if it is never used in a non-replacePrevious entry. | |
| const effectiveReplaceableRoles = []; | |
| for (const roleId of replaceableRoles) { | |
| if (!keptRoles.has(roleId)) { | |
| effectiveReplaceableRoles.push(roleId); | |
| } | |
| } | |
| return effectiveReplaceableRoles; |
| } | ||
| } | ||
| 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(); |
Copilot
AI
Feb 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The audit-log reason for adding roles is "[levels]" + localize(...) (missing a space after the tag), while the removal reason uses "[levels] " + .... Consider making these consistent so audit logs read correctly.
Summary
/manage-levels rewardssubcommands to manage rewards in Discord.Notes
reward_roles/onlyTopLevelRoleremains as fallback.Testing
/manage-levels rewards add|set|remove|clear|listand verified config updates.