add leveling feature

This commit is contained in:
Lee 2024-07-06 04:04:38 +01:00
parent 60ce8df108
commit 9d78432211
10 changed files with 462 additions and 0 deletions

@ -48,4 +48,15 @@ public final class MathUtils {
public static double lerp(double a, double b, double t) { public static double lerp(double a, double b, double t) {
return a + t * (b - a); return a + t * (b - a);
} }
/**
* Generates a random number between a minimum and maximum.
*
* @param min The minimum value.
* @param max The maximum value.
* @return The random value.
*/
public static double random(double min, double max) {
return Math.random() * (max - min) + min;
}
} }

@ -0,0 +1,52 @@
package cc.fascinated.bat.features.leveling;
import cc.fascinated.bat.features.Feature;
import cc.fascinated.bat.features.FeatureProfile;
import cc.fascinated.bat.features.leveling.command.LevelingCommand;
import cc.fascinated.bat.service.CommandService;
import lombok.NonNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
/**
* @author Fascinated (fascinated7)
*/
@Component
public class LevelingFeature extends Feature {
/**
* The cache of the required XP for each level.
*/
public static final Map<Integer, Double> xpRequiredCache = new HashMap<>();
@Autowired
public LevelingFeature(@NonNull ApplicationContext context, @NonNull CommandService commandService) {
super("Leveling", FeatureProfile.FeatureState.DISABLED, true);
super.registerCommand(commandService, context.getBean(LevelingCommand.class));
}
/**
* Gets the amount of XP needed to level up.
*
* @param level The level.
* @param currentXp The current XP.
* @return The needed XP.
*/
public static double getNeededXP(int level, double currentXp) {
return xpRequiredCache.computeIfAbsent(level, integer -> getBaseXP(level) - currentXp);
}
/**
* Gets the base XP for a level.
*
* @param level The level.
* @return The base XP.
*/
public static double getBaseXP(int level) {
return 5 * (Math.pow(level, 2)) + (50 * level) + 100;
}
}

@ -0,0 +1,54 @@
package cc.fascinated.bat.features.leveling;
import cc.fascinated.bat.common.MathUtils;
import cc.fascinated.bat.event.EventListener;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import lombok.NonNull;
import lombok.extern.log4j.Log4j2;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component
@Log4j2(topic = "Leveling Listener")
public class LevelingListener implements EventListener {
private final LevelingFeature levelingFeature;
@Autowired
public LevelingListener(@NonNull LevelingFeature levelingFeature) {
this.levelingFeature = levelingFeature;
}
@Override
public void onGuildMessageReceive(@NonNull BatGuild guild, @NonNull BatUser user, @NonNull MessageReceivedEvent event) {
if (guild.getFeatureProfile().isFeatureDisabled(levelingFeature)) { // The feature is disabled
return;
}
LevelingProfile profile = guild.getLevelingProfile();
if (profile == null) {
return;
}
UserLevel userLevel = profile.getUserLevel(user.getId());
userLevel.addXp((int) MathUtils.random(15, 25));
if (!userLevel.canLevelUp()) {
return;
}
userLevel.levelup();
TextChannel notificationChannel = profile.getNotificationChannel();
if (notificationChannel == null) {
return;
}
notificationChannel.sendMessage("Congratulations %s, you have leveled up to `%s`! :tada:".formatted(
event.getAuthor().getAsMention(),
userLevel.getLevel()
)).queue();
}
}

@ -0,0 +1,76 @@
package cc.fascinated.bat.features.leveling;
import cc.fascinated.bat.common.ChannelUtils;
import cc.fascinated.bat.common.Serializable;
import com.google.gson.Gson;
import lombok.Setter;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import org.bson.Document;
import java.util.HashMap;
import java.util.Map;
/**
* @author Fascinated (fascinated7)
*/
@Setter
public class LevelingProfile extends Serializable {
/**
* The levels of each user.
*/
private final Map<String, UserLevel> userLevels = new HashMap<>();
/**
* The id of the level up notification channel.
*/
private String notificationChannelId;
/**
* Gets the user level of a user.
*
* @param userId The user ID.
* @return The user level.
*/
public UserLevel getUserLevel(String userId) {
return userLevels.computeIfAbsent(userId, s -> new UserLevel(1, 0, -1));
}
/**
* Gets the notification channel.
*
* @return The notification channel.
*/
public TextChannel getNotificationChannel() {
return ChannelUtils.getTextChannel(notificationChannelId);
}
@Override
public void load(Document document, Gson gson) {
for (String userId : document.keySet()) {
Document userDocument = document.get(userId, Document.class);
userLevels.put(userId, new UserLevel(
userDocument.getInteger("level"),
userDocument.getDouble("currentXp"),
(long) userDocument.getOrDefault("lastXpTimestamp", (long) -1)
));
}
}
@Override
public Document serialize(Gson gson) {
Document document = new Document();
for (Map.Entry<String, UserLevel> entry : userLevels.entrySet()) {
document.put(entry.getKey(), new Document()
.append("level", entry.getValue().getLevel())
.append("currentXp", entry.getValue().getCurrentXp())
.append("lastXpTimestamp", entry.getValue().getLastXpTimestamp())
);
}
return document;
}
@Override
public void reset() {
userLevels.clear();
}
}

@ -0,0 +1,63 @@
package cc.fascinated.bat.features.leveling;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author Fascinated (fascinated7)
*/
@AllArgsConstructor
@Getter
public class UserLevel {
/**
* The current level of the user.
*/
private int level;
/**
* The current XP of the user.
*/
private double currentXp;
/**
* The timestamp of the last message sent by the user.
*/
private long lastXpTimestamp;
/**
* Adds XP to the user.
*/
public void addXp(double xp) {
this.currentXp += xp;
}
/**
* Checks if the user can level up.
*
* @return If the user can level up.
*/
public boolean canLevelUp() {
// Handle XP cooldown
long cooldown = System.currentTimeMillis() - this.lastXpTimestamp;
if (cooldown > 30_000) {
return false;
}
this.lastXpTimestamp = System.currentTimeMillis();
return this.currentXp >= LevelingFeature.getNeededXP(this.level + 1, this.currentXp);
}
/**
* Levels up the user.
*/
public void levelup() {
this.level++;
}
/**
* Resets the user's level and XP.
*/
public void reset() {
this.level = 1;
this.currentXp = 0;
}
}

@ -0,0 +1,52 @@
package cc.fascinated.bat.features.leveling.command;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.leveling.LevelingProfile;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.entities.channel.unions.GuildChannelUnion;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component("leveling.channel:sub")
@CommandInfo(
name = "channel",
description = "Sets the notification channel for leveling"
)
public class ChannelSubCommand extends BatCommand {
@Autowired
public ChannelSubCommand() {
super.addOptions(
new OptionData(OptionType.CHANNEL, "channel", "The channel to set as the notification channel", true)
);
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
OptionMapping channelOption = event.getOption("channel");
if (channelOption == null) {
return;
}
GuildChannelUnion channelUnion = channelOption.getAsChannel();
TextChannel textChannel = channelUnion.asTextChannel();
LevelingProfile profile = guild.getLevelingProfile();
profile.setNotificationChannelId(textChannel.getId());
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Successfully set the notification channel to %s".formatted(textChannel.getAsMention()))
.build()).queue();
}
}

@ -0,0 +1,62 @@
package cc.fascinated.bat.features.leveling.command;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedDescriptionBuilder;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.common.NumberFormatter;
import cc.fascinated.bat.features.leveling.LevelingFeature;
import cc.fascinated.bat.features.leveling.LevelingProfile;
import cc.fascinated.bat.features.leveling.UserLevel;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.UserService;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component("leveling.current:sub")
@CommandInfo(
name = "current",
description = "Shows the current level of the user"
)
public class CurrentSubCommand extends BatCommand {
private final UserService userService;
@Autowired
public CurrentSubCommand(@NonNull UserService userService) {
this.userService = userService;
super.addOptions(
new OptionData(OptionType.USER, "user", "The user to get the level of", false)
);
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
OptionMapping userOption = event.getOption("user");
BatUser target = userOption == null ? user : userService.getUser(userOption.getAsUser().getId());
LevelingProfile profile = guild.getLevelingProfile();
UserLevel level = profile.getUserLevel(target.getId());
EmbedDescriptionBuilder description = new EmbedDescriptionBuilder("Current Level");
description.appendLine("User: %s".formatted(target.getDiscordUser().getAsMention()), true);
description.appendLine("Level: `%s`".formatted(level.getLevel()), true);
description.appendLine("XP: `%s`/`%s`".formatted(
NumberFormatter.format(level.getCurrentXp()),
NumberFormatter.format(LevelingFeature.getBaseXP(level.getLevel()))
), true);
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription(description.build())
.setThumbnail(target.getDiscordUser().getEffectiveAvatarUrl())
.build()).queue();
}
}

@ -0,0 +1,27 @@
package cc.fascinated.bat.features.leveling.command;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
import lombok.NonNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component
@CommandInfo(
name = "leveling",
description = "The main command for the leveling feature"
)
public class LevelingCommand extends BatCommand {
@Autowired
public LevelingCommand(@NonNull ApplicationContext context) {
super.addSubCommands(
context.getBean(CurrentSubCommand.class),
context.getBean(ChannelSubCommand.class),
context.getBean(ResetSubCommand.class)
);
}
}

@ -0,0 +1,54 @@
package cc.fascinated.bat.features.leveling.command;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.leveling.LevelingProfile;
import cc.fascinated.bat.features.leveling.UserLevel;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import cc.fascinated.bat.service.UserService;
import lombok.NonNull;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component("leveling.reset:sub")
@CommandInfo(
name = "reset",
description = "Resets the level and xp of the user",
requiredPermissions = Permission.MANAGE_SERVER
)
public class ResetSubCommand extends BatCommand {
public ResetSubCommand() {
super.addOptions(
new OptionData(OptionType.USER, "user", "The user to reset", true)
);
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
OptionMapping userOption = event.getOption("user");
if (userOption == null) {
return;
}
User target = userOption.getAsUser();
LevelingProfile profile = guild.getLevelingProfile();
UserLevel level = profile.getUserLevel(target.getId());
level.reset();
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Reset Level and XP for %s!".formatted(target.getAsMention()))
.build()).queue();
}
}

@ -6,6 +6,8 @@ import cc.fascinated.bat.common.Serializable;
import cc.fascinated.bat.features.FeatureProfile; import cc.fascinated.bat.features.FeatureProfile;
import cc.fascinated.bat.features.birthday.profile.BirthdayProfile; import cc.fascinated.bat.features.birthday.profile.BirthdayProfile;
import cc.fascinated.bat.features.counter.CounterProfile; import cc.fascinated.bat.features.counter.CounterProfile;
import cc.fascinated.bat.features.leveling.LevelingFeature;
import cc.fascinated.bat.features.leveling.LevelingProfile;
import cc.fascinated.bat.features.logging.LogProfile; import cc.fascinated.bat.features.logging.LogProfile;
import cc.fascinated.bat.features.namehistory.profile.guild.NameHistoryProfile; import cc.fascinated.bat.features.namehistory.profile.guild.NameHistoryProfile;
import cc.fascinated.bat.features.reminder.ReminderProfile; import cc.fascinated.bat.features.reminder.ReminderProfile;
@ -160,6 +162,15 @@ public class BatGuild extends ProfileHolder {
return getProfile(CounterProfile.class); return getProfile(CounterProfile.class);
} }
/**
* Gets the leveling profile
*
* @return the leveling profile
*/
public LevelingProfile getLevelingProfile() {
return getProfile(LevelingProfile.class);
}
/** /**
* Saves the user * Saves the user
*/ */