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
@Singleton
LevelUpNotifier provideLevelUpNotifier(ConfigRepository configRepo, LevelService levelService) {
return new LevelUpNotifier(configRepo, levelService);
LevelRoleService provideLevelRoleService() {
return new LevelRoleService();
}
@Provides
@Singleton
LevelUpNotifier provideLevelUpNotifier(ConfigRepository configRepo, LevelService levelService, LevelRoleService levelRoleService) {
return new LevelUpNotifier(configRepo, levelService, levelRoleService);
}
@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;
import creator.loki.embed.EmbedFactory;
import creator.loki.feature.config.repository.ConfigRepository;
import creator.loki.feature.level.domain.LevelProgress;
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.channel.concrete.TextChannel;
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 final ConfigRepository configRepo;
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.levelService = levelService;
this.levelRoleService = levelRoleService;
}
/**
@ -30,27 +32,26 @@ public class LevelUpNotifier {
*/
public void sendLevelUpNotification(Member member, int oldLevel, int newLevel, MessageChannel channel) {
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 reward = getRewardForLevel(newLevel);
if (reward != null) {
embed.addField("🎁 Награда", reward, false);
String progressBar = buildProgressBar(progress.getPercentage());
String rewardRole = levelRoleService.assignRewardRole(member, newLevel);
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(
success -> logger.info("Отправлено уведомление о level up для {}: {} -> {}",
success -> logger.info("Отправлено уведомление о level up для {}: {} -> {}",
member.getEffectiveName(), oldLevel, newLevel),
error -> logger.error("Ошибка отправки уведомления о level up", error)
);
@ -83,17 +84,10 @@ public class LevelUpNotifier {
return Color.decode("#2ECC71"); // Зеленый
}
/**
* Получить награду за уровень
*/
private String getRewardForLevel(int level) {
return switch (level) {
case 5 -> "🎖️ Роль **Активный**";
case 10 -> "⭐ Роль **Постоянный**";
case 20 -> "💎 Роль **Ветеран**";
case 30 -> "👑 Роль **Легенда**";
case 50 -> "🏆 Роль **Мастер** + особый цвет ника";
default -> null;
};
private String buildProgressBar(double percentage) {
int filled = (int) (percentage / 10);
filled = Math.min(filled, 10);
int empty = 10 - filled;
return "▓".repeat(filled) + "░".repeat(empty) + " " + String.format("%.0f%%", percentage);
}
}

View file

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

View file

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