feat: wire economy LC to message/voice, add LevelRoleService, improve level-up embed

- MessageListener: +1-3 LC per message
- VoiceTracker: +1 LC/min periodic, +1 LC/2min on leave
- LevelRoleService: auto-assigns roles (Активный/Постоянный/Ветеран/Легенда/Мастер) at levels 5/10/20/30/50
- LevelUpNotifier: progress bar, role reward display, EmbedFactory chain
This commit is contained in:
loki5512344 2026-06-20 00:31:19 +02:00
parent fe121c8546
commit 283b30f979
5 changed files with 94 additions and 35 deletions

View file

@ -58,8 +58,14 @@ public class LevelModule extends AbstractModule {
@Provides @Provides
@Singleton @Singleton
LevelUpNotifier provideLevelUpNotifier(ConfigRepository configRepo, LevelService levelService) { LevelRoleService provideLevelRoleService() {
return new LevelUpNotifier(configRepo, levelService); return new LevelRoleService();
}
@Provides
@Singleton
LevelUpNotifier provideLevelUpNotifier(ConfigRepository configRepo, LevelService levelService, LevelRoleService levelRoleService) {
return new LevelUpNotifier(configRepo, levelService, levelRoleService);
} }
@Provides @Provides

View file

@ -0,0 +1,46 @@
package creator.loki.feature.level;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Role;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.Map;
public class LevelRoleService {
private static final Logger logger = LoggerFactory.getLogger(LevelRoleService.class);
private static final Map<Integer, String> REWARD_ROLES = Map.of(
5, "Активный",
10, "Постоянный",
20, "Ветеран",
30, "Легенда",
50, "Мастер"
);
public String assignRewardRole(Member member, int newLevel) {
String roleName = REWARD_ROLES.get(newLevel);
if (roleName == null) return null;
List<Role> roles = member.getGuild().getRolesByName(roleName, true);
if (roles.isEmpty()) {
logger.warn("Role '{}' not found in guild {} for level {} reward",
roleName, member.getGuild().getId(), newLevel);
return null;
}
Role role = roles.get(0);
if (member.getRoles().contains(role)) {
return null;
}
member.getGuild().addRoleToMember(member, role).queue(
success -> logger.info("Assigned role '{}' to {} for level {}",
roleName, member.getEffectiveName(), newLevel),
error -> logger.error("Failed to assign role '{}' to {}: {}",
roleName, member.getEffectiveName(), error.getMessage())
);
return roleName;
}
}

View file

@ -1,9 +1,9 @@
package creator.loki.feature.level; package creator.loki.feature.level;
import creator.loki.embed.EmbedFactory;
import creator.loki.feature.config.repository.ConfigRepository; import creator.loki.feature.config.repository.ConfigRepository;
import creator.loki.feature.level.domain.LevelProgress; import creator.loki.feature.level.domain.LevelProgress;
import creator.loki.feature.level.service.LevelService; import creator.loki.feature.level.service.LevelService;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; 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.middleman.MessageChannel;
@ -19,10 +19,12 @@ public class LevelUpNotifier {
private static final Logger logger = LoggerFactory.getLogger(LevelUpNotifier.class); private static final Logger logger = LoggerFactory.getLogger(LevelUpNotifier.class);
private final ConfigRepository configRepo; private final ConfigRepository configRepo;
private final LevelService levelService; private final LevelService levelService;
private final LevelRoleService levelRoleService;
public LevelUpNotifier(ConfigRepository configRepo, LevelService levelService) { public LevelUpNotifier(ConfigRepository configRepo, LevelService levelService, LevelRoleService levelRoleService) {
this.configRepo = configRepo; this.configRepo = configRepo;
this.levelService = levelService; this.levelService = levelService;
this.levelRoleService = levelRoleService;
} }
/** /**
@ -30,27 +32,26 @@ public class LevelUpNotifier {
*/ */
public void sendLevelUpNotification(Member member, int oldLevel, int newLevel, MessageChannel channel) { public void sendLevelUpNotification(Member member, int oldLevel, int newLevel, MessageChannel channel) {
LevelProgress progress = levelService.getProgress(member.getId()); LevelProgress progress = levelService.getProgress(member.getId());
EmbedBuilder embed = new EmbedBuilder()
.setTitle("🎉 Повышение уровня!")
.setDescription(member.getAsMention() + " достиг **уровня " + newLevel + "**!")
.addField("Предыдущий уровень", String.valueOf(oldLevel), true)
.addField("Новый уровень", String.valueOf(newLevel), true)
.addField("Всего XP", String.valueOf(progress.getTotalXP()), true)
.addField("До следующего уровня",
progress.getCurrentLevelXP() + " / " + progress.getXpForNextLevel() + " XP", false)
.setColor(getColorForLevel(newLevel))
.setThumbnail(member.getEffectiveAvatarUrl())
.setFooter("Продолжай в том же духе!");
// Добавляем награды за уровень String progressBar = buildProgressBar(progress.getPercentage());
String reward = getRewardForLevel(newLevel); String rewardRole = levelRoleService.assignRewardRole(member, newLevel);
if (reward != null) {
embed.addField("🎁 Награда", reward, false); EmbedFactory embed = EmbedFactory.create()
.withTitle("🎉 Повышение уровня!")
.withDescription(member.getAsMention() + " достиг **" + newLevel + " уровня**!")
.addField("Прогресс", progressBar, false)
.addField("Всего XP", String.valueOf(progress.getTotalXP()), true)
.addField("До следующего", progress.getCurrentLevelXP() + " / " + progress.getXpForNextLevel() + " XP", true)
.withColor(getColorForLevel(newLevel))
.withThumbnail(member.getEffectiveAvatarUrl())
.withFooter("Продолжай в том же духе!");
if (rewardRole != null) {
embed.addField("🎁 Награда", rewardRole, false);
} }
channel.sendMessageEmbeds(embed.build()).queue( channel.sendMessageEmbeds(embed.build()).queue(
success -> logger.info("Отправлено уведомление о level up для {}: {} -> {}", success -> logger.info("Отправлено уведомление о level up для {}: {} -> {}",
member.getEffectiveName(), oldLevel, newLevel), member.getEffectiveName(), oldLevel, newLevel),
error -> logger.error("Ошибка отправки уведомления о level up", error) error -> logger.error("Ошибка отправки уведомления о level up", error)
); );
@ -83,17 +84,10 @@ public class LevelUpNotifier {
return Color.decode("#2ECC71"); // Зеленый return Color.decode("#2ECC71"); // Зеленый
} }
/** private String buildProgressBar(double percentage) {
* Получить награду за уровень int filled = (int) (percentage / 10);
*/ filled = Math.min(filled, 10);
private String getRewardForLevel(int level) { int empty = 10 - filled;
return switch (level) { return "▓".repeat(filled) + "░".repeat(empty) + " " + String.format("%.0f%%", percentage);
case 5 -> "🎖️ Роль **Активный**";
case 10 -> "⭐ Роль **Постоянный**";
case 20 -> "💎 Роль **Ветеран**";
case 30 -> "👑 Роль **Легенда**";
case 50 -> "🏆 Роль **Мастер** + особый цвет ника";
default -> null;
};
} }
} }

View file

@ -3,21 +3,26 @@ package creator.loki.feature.level;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.Singleton; import com.google.inject.Singleton;
import creator.loki.core.EventDispatcher; import creator.loki.core.EventDispatcher;
import creator.loki.feature.economy.service.EconomyService;
import creator.loki.feature.level.service.LevelService; import creator.loki.feature.level.service.LevelService;
import creator.loki.feature.level.domain.LevelUpResult; import creator.loki.feature.level.domain.LevelUpResult;
import creator.loki.embed.EmbedTemplates; import creator.loki.embed.EmbedTemplates;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent; import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter; import net.dv8tion.jda.api.hooks.ListenerAdapter;
import java.util.concurrent.ThreadLocalRandom;
@Singleton @Singleton
public class MessageListener extends ListenerAdapter { public class MessageListener extends ListenerAdapter {
private final EventDispatcher dispatcher; private final EventDispatcher dispatcher;
private final LevelService levelService; private final LevelService levelService;
private final EconomyService economyService;
@Inject @Inject
public MessageListener(EventDispatcher dispatcher, LevelService levelService) { public MessageListener(EventDispatcher dispatcher, LevelService levelService, EconomyService economyService) {
this.dispatcher = dispatcher; this.dispatcher = dispatcher;
this.levelService = levelService; this.levelService = levelService;
this.economyService = economyService;
} }
@Override @Override
@ -38,6 +43,9 @@ public class MessageListener extends ListenerAdapter {
) )
).queue(); ).queue();
} }
int lcAmount = ThreadLocalRandom.current().nextInt(1, 4);
economyService.earnCash(userId, lcAmount);
} }
dispatcher.handleMessage(event); dispatcher.handleMessage(event);

View file

@ -2,6 +2,7 @@ package creator.loki.feature.level;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.Singleton; import com.google.inject.Singleton;
import creator.loki.feature.economy.service.EconomyService;
import creator.loki.feature.level.domain.LevelUpResult; import creator.loki.feature.level.domain.LevelUpResult;
import creator.loki.feature.level.service.LevelService; import creator.loki.feature.level.service.LevelService;
import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Member;
@ -23,12 +24,14 @@ import java.util.concurrent.TimeUnit;
public class VoiceTracker extends ListenerAdapter { public class VoiceTracker extends ListenerAdapter {
private static final Logger logger = LoggerFactory.getLogger(VoiceTracker.class); private static final Logger logger = LoggerFactory.getLogger(VoiceTracker.class);
private final LevelService levelService; private final LevelService levelService;
private final EconomyService economyService;
private final Map<String, Long> voiceJoinTimes = new ConcurrentHashMap<>(); private final Map<String, Long> voiceJoinTimes = new ConcurrentHashMap<>();
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
@Inject @Inject
public VoiceTracker(LevelService levelService) { public VoiceTracker(LevelService levelService, EconomyService economyService) {
this.levelService = levelService; this.levelService = levelService;
this.economyService = economyService;
// Каждую минуту начисляем XP тем, кто в войсе // Каждую минуту начисляем XP тем, кто в войсе
scheduler.scheduleAtFixedRate(() -> Thread.startVirtualThread(this::updateVoiceTime), 1, 1, TimeUnit.MINUTES); scheduler.scheduleAtFixedRate(() -> Thread.startVirtualThread(this::updateVoiceTime), 1, 1, TimeUnit.MINUTES);
@ -58,6 +61,7 @@ public class VoiceTracker extends ListenerAdapter {
if (minutes > 0) { if (minutes > 0) {
levelService.addVoiceXP(userId, minutes); levelService.addVoiceXP(userId, minutes);
economyService.earnCash(userId, minutes / 2);
logger.debug("Пользователь {} провёл в войсе {} минут", member.getEffectiveName(), minutes); logger.debug("Пользователь {} провёл в войсе {} минут", member.getEffectiveName(), minutes);
} }
} }
@ -84,6 +88,7 @@ public class VoiceTracker extends ListenerAdapter {
if (minutes >= 1) { if (minutes >= 1) {
// Начисляем XP за каждую минуту // Начисляем XP за каждую минуту
levelService.addVoiceXP(userId, 1); levelService.addVoiceXP(userId, 1);
economyService.earnCash(userId, 1);
// Обновляем время входа // Обновляем время входа
voiceJoinTimes.put(userId, now); voiceJoinTimes.put(userId, now);
} }