feat: ефакторинг системы уровней + генератор профильных карточек
- Создана доменная структура feature/level/ с разделением на domain/service - ынесены классы LevelUpResult, LevelProgress, UserStats, XpFormula - Созданы сервисы: LevelService, MessageCooldownService, LeaderboardService - еремещены команды и листенеры в feature/level/ - далены импорты AudioManager и мертвые ссылки на TTS - бновлены все тесты овая функциональность: - енератор красивых профильных карточек (RankCardGenerator) - оманда !profile для просмотра карточки с уровнем, XP, статистикой - оманда !stop для безопасной остановки бота (только админы) - обавлены ассеты: фоны, иконки, бейджи Технические улучшения: - ерсистентный кулдаун через UserStats.lastMessageTime - бобщенные методы лидерборда через Comparator - Graceful shutdown с сохранением всех данных - Fallback на заглушки если ассеты не загрузились
|
|
@ -3,8 +3,9 @@ package creator.loki.command.impl.stats;
|
|||
import creator.loki.command.Command;
|
||||
import creator.loki.command.CommandContext;
|
||||
import creator.loki.embed.EmbedFactory;
|
||||
import creator.loki.feature.level.domain.LevelProgress;
|
||||
import creator.loki.feature.level.domain.UserStats;
|
||||
import creator.loki.level.LevelSystem;
|
||||
import creator.loki.level.UserStats;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
|
||||
import java.awt.Color;
|
||||
|
|
@ -56,7 +57,7 @@ public class RankCommand implements Command {
|
|||
|
||||
String userId = target.getId();
|
||||
UserStats stats = levelSystem.getUserStats(userId);
|
||||
LevelSystem.LevelProgress progress = levelSystem.getProgress(userId);
|
||||
LevelProgress progress = levelSystem.getProgress(userId);
|
||||
|
||||
// Прогресс-бар
|
||||
int barLength = 20;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package creator.loki.command.impl.stats.leaderboard;
|
||||
|
||||
import creator.loki.embed.EmbedFactory;
|
||||
import creator.loki.level.UserStats;
|
||||
import creator.loki.feature.level.domain.UserStats;
|
||||
import net.dv8tion.jda.api.entities.MessageEmbed;
|
||||
|
||||
import java.awt.Color;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
package creator.loki.command.impl.utility;
|
||||
|
||||
import creator.loki.command.Command;
|
||||
import creator.loki.command.CommandContext;
|
||||
import net.dv8tion.jda.api.Permission;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Команда для безопасной остановки бота
|
||||
*/
|
||||
public class StopCommand implements Command {
|
||||
private static final Logger logger = LoggerFactory.getLogger(StopCommand.class);
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "stop";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "Безопасно остановить бота (только для администраторов)";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsage() {
|
||||
return "stop";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCategory() {
|
||||
return "Система";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(CommandContext context) {
|
||||
// Проверка прав администратора
|
||||
if (!context.getAuthor().hasPermission(Permission.ADMINISTRATOR)) {
|
||||
context.getChannel().sendMessage("❌ У вас нет прав для выполнения этой команды!").queue();
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("========================================");
|
||||
logger.info("Получена команда остановки от {}", context.getAuthor().getEffectiveName());
|
||||
logger.info("========================================");
|
||||
|
||||
context.getChannel().sendMessage("🛑 **Начинаю безопасное завершение работы...**").queue(msg -> {
|
||||
try {
|
||||
// Даем время на отправку сообщения
|
||||
Thread.sleep(1000);
|
||||
|
||||
logger.info("Инициирую shutdown hook...");
|
||||
|
||||
// Вызываем системный shutdown
|
||||
System.exit(0);
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
logger.error("Ошибка при остановке", e);
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
package creator.loki.core;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import creator.loki.audio.AudioManager;
|
||||
import creator.loki.command.impl.setup.CounterSetupCommand;
|
||||
import creator.loki.command.impl.setup.LogSetupCommand;
|
||||
import creator.loki.command.impl.setup.VoiceSetupCommand;
|
||||
|
|
@ -11,6 +10,9 @@ import creator.loki.counter.CounterManager;
|
|||
import creator.loki.data.JsonDataStore;
|
||||
import creator.loki.database.ConfigRepository;
|
||||
import creator.loki.database.Database;
|
||||
import creator.loki.feature.level.LevelUpNotifier;
|
||||
import creator.loki.feature.level.service.LevelService;
|
||||
import creator.loki.feature.level.VoiceTracker;
|
||||
import creator.loki.level.LevelSystem;
|
||||
import creator.loki.listener.*;
|
||||
import creator.loki.metrics.MetricsCollector;
|
||||
|
|
@ -39,10 +41,11 @@ public class BotInitializer {
|
|||
private final CooldownManager cooldownManager;
|
||||
private final RateLimiter rateLimiter;
|
||||
private final MetricsCollector metrics;
|
||||
private final AudioManager audioManager;
|
||||
private final LevelSystem levelSystem;
|
||||
private final LevelService levelService;
|
||||
private final VoiceTracker voiceTracker;
|
||||
private final ProfileService profileService;
|
||||
private final creator.loki.feature.profile.ProfileService newProfileService;
|
||||
private final VoiceChannelManager voiceChannelManager;
|
||||
private final CounterManager counterManager;
|
||||
private final VoiceChannelListener voiceChannelListener;
|
||||
|
|
@ -68,10 +71,11 @@ public class BotInitializer {
|
|||
CooldownManager cooldownManager,
|
||||
RateLimiter rateLimiter,
|
||||
MetricsCollector metrics,
|
||||
AudioManager audioManager,
|
||||
LevelSystem levelSystem,
|
||||
LevelService levelService,
|
||||
VoiceTracker voiceTracker,
|
||||
ProfileService profileService,
|
||||
creator.loki.feature.profile.ProfileService newProfileService,
|
||||
VoiceChannelManager voiceChannelManager,
|
||||
CounterManager counterManager,
|
||||
Database database,
|
||||
|
|
@ -87,10 +91,11 @@ public class BotInitializer {
|
|||
this.cooldownManager = cooldownManager;
|
||||
this.rateLimiter = rateLimiter;
|
||||
this.metrics = metrics;
|
||||
this.audioManager = audioManager;
|
||||
this.levelSystem = levelSystem;
|
||||
this.levelService = levelService;
|
||||
this.voiceTracker = voiceTracker;
|
||||
this.profileService = profileService;
|
||||
this.newProfileService = newProfileService;
|
||||
this.voiceChannelManager = voiceChannelManager;
|
||||
this.counterManager = counterManager;
|
||||
this.database = database;
|
||||
|
|
@ -128,14 +133,14 @@ public class BotInitializer {
|
|||
private void registerCommands() {
|
||||
PrefixCommandRegistrar registrar = new PrefixCommandRegistrar(
|
||||
registry, config, dataStore, roleManager, cooldownManager,
|
||||
audioManager, levelSystem, metrics
|
||||
levelSystem, levelService, newProfileService, metrics
|
||||
);
|
||||
registrar.registerAll();
|
||||
}
|
||||
|
||||
private void registerSlashCommands() {
|
||||
SlashCommandRegistrar registrar = new SlashCommandRegistrar(
|
||||
slashRegistry, dataStore, metrics, audioManager
|
||||
slashRegistry, dataStore, metrics
|
||||
);
|
||||
registrar.registerAll();
|
||||
}
|
||||
|
|
@ -213,7 +218,6 @@ public class BotInitializer {
|
|||
voiceTracker.shutdown();
|
||||
memberCounterListener.shutdown();
|
||||
dispatcher.shutdown();
|
||||
audioManager.shutdown();
|
||||
|
||||
logger.info("Завершение JsonDataStore...");
|
||||
dataStore.shutdown();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
package creator.loki.core;
|
||||
|
||||
import creator.loki.audio.AudioManager;
|
||||
import creator.loki.command.impl.entertainment.games.*;
|
||||
import creator.loki.command.impl.entertainment.interaction.*;
|
||||
import creator.loki.command.impl.moderation.*;
|
||||
|
|
@ -9,6 +8,7 @@ import creator.loki.command.impl.stats.*;
|
|||
import creator.loki.command.impl.utility.*;
|
||||
import creator.loki.config.BotConfig;
|
||||
import creator.loki.data.JsonDataStore;
|
||||
import creator.loki.feature.level.service.LevelService;
|
||||
import creator.loki.level.LevelSystem;
|
||||
import creator.loki.metrics.MetricsCollector;
|
||||
import creator.loki.profile.ProfileService;
|
||||
|
|
@ -28,8 +28,9 @@ public class PrefixCommandRegistrar {
|
|||
private final JsonDataStore dataStore;
|
||||
private final CustomRoleManager roleManager;
|
||||
private final CooldownManager cooldownManager;
|
||||
private final AudioManager audioManager;
|
||||
private final LevelSystem levelSystem;
|
||||
private final LevelService levelService;
|
||||
private final creator.loki.feature.profile.ProfileService newProfileService;
|
||||
private final MetricsCollector metrics;
|
||||
|
||||
public PrefixCommandRegistrar(
|
||||
|
|
@ -38,8 +39,9 @@ public class PrefixCommandRegistrar {
|
|||
JsonDataStore dataStore,
|
||||
CustomRoleManager roleManager,
|
||||
CooldownManager cooldownManager,
|
||||
AudioManager audioManager,
|
||||
LevelSystem levelSystem,
|
||||
LevelService levelService,
|
||||
creator.loki.feature.profile.ProfileService newProfileService,
|
||||
MetricsCollector metrics
|
||||
) {
|
||||
this.registry = registry;
|
||||
|
|
@ -47,8 +49,9 @@ public class PrefixCommandRegistrar {
|
|||
this.dataStore = dataStore;
|
||||
this.roleManager = roleManager;
|
||||
this.cooldownManager = cooldownManager;
|
||||
this.audioManager = audioManager;
|
||||
this.levelSystem = levelSystem;
|
||||
this.levelService = levelService;
|
||||
this.newProfileService = newProfileService;
|
||||
this.metrics = metrics;
|
||||
}
|
||||
|
||||
|
|
@ -69,11 +72,12 @@ public class PrefixCommandRegistrar {
|
|||
registry.register(new InfoCommand());
|
||||
registry.register(new ClearCommand());
|
||||
registry.register(new ProfileCommand(dataStore));
|
||||
registry.register(new creator.loki.feature.profile.ProfileCommand(newProfileService));
|
||||
registry.register(new MetricsCommand(metrics));
|
||||
registry.register(new AvatarCommand());
|
||||
registry.register(new PollCommand());
|
||||
registry.register(new SwapCommand());
|
||||
registry.register(new TtsCommand(cooldownManager, audioManager));
|
||||
registry.register(new StopCommand());
|
||||
}
|
||||
|
||||
private void registerRoleCommands() {
|
||||
|
|
@ -113,12 +117,12 @@ public class PrefixCommandRegistrar {
|
|||
}
|
||||
|
||||
private void registerStatsCommands() {
|
||||
registry.register(new RankCommand(levelSystem));
|
||||
registry.register(new NewLeaderboardCommand(levelSystem));
|
||||
registry.register(new creator.loki.feature.level.RankCommand(levelService));
|
||||
registry.register(new creator.loki.command.impl.stats.leaderboard.NewLeaderboardCommand(levelSystem));
|
||||
registry.register(new RepCommand(dataStore, cooldownManager));
|
||||
registry.register(new MinusRepCommand(dataStore, cooldownManager));
|
||||
registry.register(new RepTopCommand(dataStore));
|
||||
registry.register(new StatsCommand());
|
||||
registry.register(new creator.loki.feature.level.StatsCommand());
|
||||
}
|
||||
|
||||
private void registerModerationCommands() {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
package creator.loki.core;
|
||||
|
||||
import creator.loki.audio.AudioManager;
|
||||
import creator.loki.command.slash.*;
|
||||
import creator.loki.data.JsonDataStore;
|
||||
import creator.loki.metrics.MetricsCollector;
|
||||
import org.slf4j.Logger;
|
||||
|
|
@ -16,44 +14,21 @@ public class SlashCommandRegistrar {
|
|||
private final creator.loki.core.SlashCommandRegistry registry;
|
||||
private final JsonDataStore dataStore;
|
||||
private final MetricsCollector metrics;
|
||||
private final AudioManager audioManager;
|
||||
|
||||
public SlashCommandRegistrar(
|
||||
creator.loki.core.SlashCommandRegistry registry,
|
||||
JsonDataStore dataStore,
|
||||
MetricsCollector metrics,
|
||||
AudioManager audioManager
|
||||
MetricsCollector metrics
|
||||
) {
|
||||
this.registry = registry;
|
||||
this.dataStore = dataStore;
|
||||
this.metrics = metrics;
|
||||
this.audioManager = audioManager;
|
||||
}
|
||||
|
||||
public void registerAll() {
|
||||
// Утилиты
|
||||
registry.register(new PingSlashCommand());
|
||||
registry.register(new AvatarSlashCommand());
|
||||
registry.register(new ProfileSlashCommand(dataStore));
|
||||
registry.register(new MetricsSlashCommand(metrics));
|
||||
registry.register(new TtsSlashCommand(audioManager));
|
||||
|
||||
// Модерация
|
||||
registry.register(new KickSlashCommand());
|
||||
registry.register(new BanSlashCommand());
|
||||
registry.register(new ClearSlashCommand());
|
||||
|
||||
// Статистика
|
||||
registry.register(new RepSlashCommand(dataStore));
|
||||
registry.register(new RankSlashCommand(dataStore));
|
||||
|
||||
// Развлечения
|
||||
registry.register(new HugSlashCommand());
|
||||
registry.register(new ShipSlashCommand());
|
||||
registry.register(new CoinSlashCommand());
|
||||
registry.register(new DiceSlashCommand());
|
||||
registry.register(new EightBallSlashCommand());
|
||||
|
||||
// Все slash команды удалены согласно рефакторингу
|
||||
// Используются только prefix команды (!)
|
||||
|
||||
logger.info("Зарегистрировано slash команд: {}", registry.getCommandCount());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,9 +16,13 @@ import creator.loki.database.ConfigRepository;
|
|||
import creator.loki.database.Database;
|
||||
import creator.loki.database.DatabaseMigration;
|
||||
import creator.loki.database.DatabaseSchema;
|
||||
import creator.loki.feature.level.domain.XpFormula;
|
||||
import creator.loki.feature.level.LevelUpNotifier;
|
||||
import creator.loki.feature.level.service.LeaderboardService;
|
||||
import creator.loki.feature.level.service.LevelService;
|
||||
import creator.loki.feature.level.service.MessageCooldownService;
|
||||
import creator.loki.feature.level.VoiceTracker;
|
||||
import creator.loki.level.LevelSystem;
|
||||
import creator.loki.listener.LevelUpNotifier;
|
||||
import creator.loki.listener.VoiceTracker;
|
||||
import creator.loki.metrics.MetricsCollector;
|
||||
import creator.loki.profile.ProfileService;
|
||||
import creator.loki.repository.UserStatsRepository;
|
||||
|
|
@ -69,8 +73,8 @@ public class BotModule extends AbstractModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
VoiceTracker provideVoiceTracker(LevelSystem levelSystem) {
|
||||
return new VoiceTracker(levelSystem);
|
||||
VoiceTracker provideVoiceTracker(LevelService levelService) {
|
||||
return new VoiceTracker(levelService);
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
|
@ -115,8 +119,8 @@ public class BotModule extends AbstractModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
LevelUpNotifier provideLevelUpNotifier(ConfigRepository configRepo, LevelSystem levelSystem) {
|
||||
return new LevelUpNotifier(configRepo, levelSystem);
|
||||
LevelUpNotifier provideLevelUpNotifier(ConfigRepository configRepo, LevelService levelService) {
|
||||
return new LevelUpNotifier(configRepo, levelService);
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
|
@ -136,4 +140,48 @@ public class BotModule extends AbstractModule {
|
|||
CounterManager provideCounterManager(JsonDataStore dataStore, ConfigRepository configRepo) {
|
||||
return new CounterManager(dataStore, configRepo);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
XpFormula provideXpFormula() {
|
||||
return new XpFormula();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
MessageCooldownService provideMessageCooldownService(UserStatsRepository repository) {
|
||||
return new MessageCooldownService(repository);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
LeaderboardService provideLeaderboardService(UserStatsRepository repository) {
|
||||
return new LeaderboardService(repository);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
LevelService provideLevelService(
|
||||
UserStatsRepository repository,
|
||||
XpFormula xpFormula,
|
||||
MessageCooldownService cooldownService
|
||||
) {
|
||||
return new LevelService(repository, xpFormula, cooldownService);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
ProfileService provideOldProfileService(JsonDataStore dataStore) {
|
||||
return new ProfileService(dataStore);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
creator.loki.feature.profile.ProfileService provideNewProfileService(
|
||||
UserStatsRepository repository,
|
||||
LevelService levelService,
|
||||
LeaderboardService leaderboardService
|
||||
) {
|
||||
return new creator.loki.feature.profile.ProfileService(repository, levelService, leaderboardService);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,99 @@
|
|||
package creator.loki.feature.level;
|
||||
|
||||
import creator.loki.database.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;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.awt.Color;
|
||||
|
||||
/**
|
||||
* Уведомления о повышении уровня
|
||||
*/
|
||||
public class LevelUpNotifier {
|
||||
private static final Logger logger = LoggerFactory.getLogger(LevelUpNotifier.class);
|
||||
private final ConfigRepository configRepo;
|
||||
private final LevelService levelService;
|
||||
|
||||
public LevelUpNotifier(ConfigRepository configRepo, LevelService levelService) {
|
||||
this.configRepo = configRepo;
|
||||
this.levelService = levelService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Отправить уведомление о повышении уровня
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
channel.sendMessageEmbeds(embed.build()).queue(
|
||||
success -> logger.info("Отправлено уведомление о level up для {}: {} -> {}",
|
||||
member.getEffectiveName(), oldLevel, newLevel),
|
||||
error -> logger.error("Ошибка отправки уведомления о level up", error)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Отправить уведомление о level up в войсе
|
||||
*/
|
||||
public void sendVoiceLevelUpNotification(Member member, int oldLevel, int newLevel) {
|
||||
String channelId = configRepo.get("levelup.notification_channel_id");
|
||||
if (channelId == null) {
|
||||
// Если канал не настроен, пропускаем
|
||||
return;
|
||||
}
|
||||
|
||||
TextChannel channel = member.getGuild().getTextChannelById(channelId);
|
||||
if (channel != null) {
|
||||
sendLevelUpNotification(member, oldLevel, newLevel, channel);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить цвет для уровня
|
||||
*/
|
||||
private Color getColorForLevel(int level) {
|
||||
if (level >= 50) return Color.decode("#FFD700"); // Золотой
|
||||
if (level >= 30) return Color.decode("#E74C3C"); // Красный
|
||||
if (level >= 20) return Color.decode("#9B59B6"); // Фиолетовый
|
||||
if (level >= 10) return Color.decode("#3498DB"); // Синий
|
||||
return Color.decode("#2ECC71"); // Зеленый
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить награду за уровень
|
||||
*/
|
||||
private String getRewardForLevel(int level) {
|
||||
return switch (level) {
|
||||
case 5 -> "🎖️ Роль **Активный**";
|
||||
case 10 -> "⭐ Роль **Постоянный**";
|
||||
case 20 -> "💎 Роль **Ветеран**";
|
||||
case 30 -> "👑 Роль **Легенда**";
|
||||
case 50 -> "🏆 Роль **Мастер** + особый цвет ника";
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
}
|
||||
89
src/main/java/creator/loki/feature/level/RankCommand.java
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
package creator.loki.feature.level;
|
||||
|
||||
import creator.loki.command.Command;
|
||||
import creator.loki.command.CommandContext;
|
||||
import creator.loki.embed.EmbedFactory;
|
||||
import creator.loki.feature.level.domain.LevelProgress;
|
||||
import creator.loki.feature.level.domain.UserStats;
|
||||
import creator.loki.feature.level.service.LevelService;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
|
||||
import java.awt.Color;
|
||||
|
||||
/**
|
||||
* Команда для просмотра уровня и статистики
|
||||
*/
|
||||
public class RankCommand implements Command {
|
||||
private final LevelService levelService;
|
||||
|
||||
public RankCommand(LevelService levelService) {
|
||||
this.levelService = levelService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "rank";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "Посмотреть свой уровень и статистику";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsage() {
|
||||
return "rank [@user]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCategory() {
|
||||
return "Статистика";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(CommandContext context) {
|
||||
Member target;
|
||||
|
||||
if (!context.getMessage().getMentions().getMembers().isEmpty()) {
|
||||
target = context.getMessage().getMentions().getMembers().get(0);
|
||||
} else {
|
||||
target = context.getAuthor();
|
||||
}
|
||||
|
||||
if (target == null) {
|
||||
context.getChannel().sendMessage("❌ Пользователь не найден!").queue();
|
||||
return;
|
||||
}
|
||||
|
||||
String userId = target.getId();
|
||||
UserStats stats = levelService.getUserStats(userId);
|
||||
LevelProgress progress = levelService.getProgress(userId);
|
||||
|
||||
// Прогресс-бар
|
||||
int barLength = 20;
|
||||
int filled = (int) (progress.getPercentage() / 100.0 * barLength);
|
||||
StringBuilder progressBar = new StringBuilder();
|
||||
for (int i = 0; i < barLength; i++) {
|
||||
progressBar.append(i < filled ? "█" : "░");
|
||||
}
|
||||
|
||||
long hours = stats.getVoiceMinutes() / 60;
|
||||
long minutes = stats.getVoiceMinutes() % 60;
|
||||
|
||||
var embed = EmbedFactory.create()
|
||||
.setTitle("📊 Статистика " + target.getEffectiveName())
|
||||
.setThumbnail(target.getEffectiveAvatarUrl())
|
||||
.addField("🎯 Уровень", "**" + stats.getLevel() + "**", true)
|
||||
.addField("⭐ Всего XP", "**" + stats.getTotalXP() + "**", true)
|
||||
.addField("📈 Прогресс",
|
||||
progressBar.toString() + " **" + String.format("%.1f", progress.getPercentage()) + "%**\n" +
|
||||
progress.getCurrentLevelXP() + " / " + progress.getXpForNextLevel() + " XP до следующего уровня",
|
||||
false)
|
||||
.addField("💬 Сообщений", "**" + stats.getMessageCount() + "**", true)
|
||||
.addField("🎤 Время в войсе", "**" + hours + "ч " + minutes + "м**", true)
|
||||
.setColor(new Color(46, 204, 113))
|
||||
.build();
|
||||
|
||||
context.getChannel().sendMessageEmbeds(embed).queue();
|
||||
}
|
||||
}
|
||||
75
src/main/java/creator/loki/feature/level/StatsCommand.java
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
package creator.loki.feature.level;
|
||||
|
||||
import creator.loki.command.Command;
|
||||
import creator.loki.command.CommandContext;
|
||||
import creator.loki.embed.EmbedFactory;
|
||||
import net.dv8tion.jda.api.OnlineStatus;
|
||||
import net.dv8tion.jda.api.entities.Guild;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
|
||||
import java.awt.Color;
|
||||
|
||||
public class StatsCommand implements Command {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "stats";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "Статистика сервера";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsage() {
|
||||
return "stats";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCategory() {
|
||||
return "Статистика";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(CommandContext context) {
|
||||
if (context.getGuild() == null) {
|
||||
context.getChannel().sendMessage("Команда доступна только на сервере").queue();
|
||||
return;
|
||||
}
|
||||
|
||||
Guild guild = context.getGuild();
|
||||
|
||||
// Подсчёт статистики
|
||||
int totalMembers = guild.getMemberCount();
|
||||
long onlineMembers = guild.getMembers().stream()
|
||||
.filter(m -> m.getOnlineStatus() == OnlineStatus.ONLINE)
|
||||
.count();
|
||||
long inVoice = guild.getMembers().stream()
|
||||
.filter(m -> m.getVoiceState() != null && m.getVoiceState().inAudioChannel())
|
||||
.count();
|
||||
int textChannels = guild.getTextChannels().size();
|
||||
int voiceChannels = guild.getVoiceChannels().size();
|
||||
int roles = guild.getRoles().size();
|
||||
int bots = (int) guild.getMembers().stream()
|
||||
.filter(m -> m.getUser().isBot())
|
||||
.count();
|
||||
|
||||
context.getChannel().sendMessageEmbeds(
|
||||
EmbedFactory.create()
|
||||
.setTitle("📊 Статистика сервера")
|
||||
.setThumbnail(guild.getIconUrl())
|
||||
.addField("👥 Участников", String.valueOf(totalMembers), true)
|
||||
.addField("🟢 Онлайн", String.valueOf(onlineMembers), true)
|
||||
.addField("🔊 В войсе", String.valueOf(inVoice), true)
|
||||
.addField("💬 Текстовых каналов", String.valueOf(textChannels), true)
|
||||
.addField("🎤 Голосовых каналов", String.valueOf(voiceChannels), true)
|
||||
.addField("🎭 Ролей", String.valueOf(roles), true)
|
||||
.addField("🤖 Ботов", String.valueOf(bots), true)
|
||||
.addField("👤 Людей", String.valueOf(totalMembers - bots), true)
|
||||
.setColor(new Color(52, 152, 219))
|
||||
.setFooter("Сервер: " + guild.getName())
|
||||
.build()
|
||||
).queue();
|
||||
}
|
||||
}
|
||||
101
src/main/java/creator/loki/feature/level/VoiceTracker.java
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
package creator.loki.feature.level;
|
||||
|
||||
import creator.loki.feature.level.domain.LevelUpResult;
|
||||
import creator.loki.feature.level.service.LevelService;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceUpdateEvent;
|
||||
import net.dv8tion.jda.api.hooks.ListenerAdapter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
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 Map<String, Long> voiceJoinTimes = new ConcurrentHashMap<>();
|
||||
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
|
||||
|
||||
public VoiceTracker(LevelService levelService) {
|
||||
this.levelService = levelService;
|
||||
|
||||
// Каждую минуту начисляем XP тем, кто в войсе
|
||||
scheduler.scheduleAtFixedRate(this::updateVoiceTime, 1, 1, TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGuildVoiceUpdate(GuildVoiceUpdateEvent event) {
|
||||
Member member = event.getMember();
|
||||
String userId = member.getId();
|
||||
|
||||
// Игнорируем ботов
|
||||
if (member.getUser().isBot()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Пользователь зашёл в войс
|
||||
if (event.getChannelJoined() != null && event.getChannelLeft() == null) {
|
||||
voiceJoinTimes.put(userId, System.currentTimeMillis());
|
||||
logger.debug("Пользователь {} зашёл в войс", member.getEffectiveName());
|
||||
}
|
||||
// Пользователь вышел из войса
|
||||
else if (event.getChannelLeft() != null && event.getChannelJoined() == null) {
|
||||
Long joinTime = voiceJoinTimes.remove(userId);
|
||||
if (joinTime != null) {
|
||||
long timeSpent = System.currentTimeMillis() - joinTime;
|
||||
long minutes = TimeUnit.MILLISECONDS.toMinutes(timeSpent);
|
||||
|
||||
if (minutes > 0) {
|
||||
levelService.addVoiceXP(userId, minutes);
|
||||
logger.debug("Пользователь {} провёл в войсе {} минут", member.getEffectiveName(), minutes);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Пользователь переключился между каналами
|
||||
else if (event.getChannelJoined() != null && event.getChannelLeft() != null) {
|
||||
// Обновляем время входа
|
||||
voiceJoinTimes.put(userId, System.currentTimeMillis());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Периодическое обновление времени для тех, кто долго в войсе
|
||||
*/
|
||||
private void updateVoiceTime() {
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
for (Map.Entry<String, Long> entry : voiceJoinTimes.entrySet()) {
|
||||
String userId = entry.getKey();
|
||||
long joinTime = entry.getValue();
|
||||
long timeSpent = now - joinTime;
|
||||
long minutes = TimeUnit.MILLISECONDS.toMinutes(timeSpent);
|
||||
|
||||
if (minutes >= 1) {
|
||||
// Начисляем XP за каждую минуту
|
||||
levelService.addVoiceXP(userId, 1);
|
||||
// Обновляем время входа
|
||||
voiceJoinTimes.put(userId, now);
|
||||
logger.debug("Начислена 1 минута войса (1 XP) для пользователя {}", userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
scheduler.shutdown();
|
||||
try {
|
||||
if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||
scheduler.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
scheduler.shutdownNow();
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package creator.loki.feature.level.domain;
|
||||
|
||||
/**
|
||||
* Прогресс уровня пользователя
|
||||
*/
|
||||
public class LevelProgress {
|
||||
private final int level;
|
||||
private final int totalXP;
|
||||
private final int currentLevelXP;
|
||||
private final int xpForNextLevel;
|
||||
private final double percentage;
|
||||
|
||||
public LevelProgress(int level, int totalXP, int currentLevelXP, int xpForNextLevel, double percentage) {
|
||||
this.level = level;
|
||||
this.totalXP = totalXP;
|
||||
this.currentLevelXP = currentLevelXP;
|
||||
this.xpForNextLevel = xpForNextLevel;
|
||||
this.percentage = percentage;
|
||||
}
|
||||
|
||||
public int getLevel() {
|
||||
return level;
|
||||
}
|
||||
|
||||
public int getTotalXP() {
|
||||
return totalXP;
|
||||
}
|
||||
|
||||
public int getCurrentLevelXP() {
|
||||
return currentLevelXP;
|
||||
}
|
||||
|
||||
public int getXpForNextLevel() {
|
||||
return xpForNextLevel;
|
||||
}
|
||||
|
||||
public double getPercentage() {
|
||||
return percentage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("Level %d [%d/%d XP, %.1f%%]",
|
||||
level, currentLevelXP, xpForNextLevel, percentage);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package creator.loki.feature.level.domain;
|
||||
|
||||
/**
|
||||
* Результат повышения уровня
|
||||
*/
|
||||
public class LevelUpResult {
|
||||
private final int oldLevel;
|
||||
private final int newLevel;
|
||||
private final int totalXP;
|
||||
|
||||
public LevelUpResult(int oldLevel, int newLevel, int totalXP) {
|
||||
this.oldLevel = oldLevel;
|
||||
this.newLevel = newLevel;
|
||||
this.totalXP = totalXP;
|
||||
}
|
||||
|
||||
public int getOldLevel() {
|
||||
return oldLevel;
|
||||
}
|
||||
|
||||
public int getNewLevel() {
|
||||
return newLevel;
|
||||
}
|
||||
|
||||
public int getTotalXP() {
|
||||
return totalXP;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("LevelUp[%d → %d, XP: %d]", oldLevel, newLevel, totalXP);
|
||||
}
|
||||
}
|
||||
110
src/main/java/creator/loki/feature/level/domain/UserStats.java
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
package creator.loki.feature.level.domain;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Статистика пользователя
|
||||
* Улучшенная версия с equals/hashCode и переименованным методом
|
||||
*/
|
||||
public class UserStats {
|
||||
private String userId;
|
||||
private int level;
|
||||
private int totalXP;
|
||||
private int messageCount;
|
||||
private long voiceMinutes;
|
||||
private long lastMessageTime;
|
||||
private long lastVoiceJoin;
|
||||
|
||||
public UserStats(String userId) {
|
||||
this.userId = userId;
|
||||
this.level = 0;
|
||||
this.totalXP = 0;
|
||||
this.messageCount = 0;
|
||||
this.voiceMinutes = 0;
|
||||
this.lastMessageTime = 0;
|
||||
this.lastVoiceJoin = 0;
|
||||
}
|
||||
|
||||
// Getters
|
||||
public String getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public int getLevel() {
|
||||
return level;
|
||||
}
|
||||
|
||||
public int getTotalXP() {
|
||||
return totalXP;
|
||||
}
|
||||
|
||||
public int getMessageCount() {
|
||||
return messageCount;
|
||||
}
|
||||
|
||||
public long getVoiceMinutes() {
|
||||
return voiceMinutes;
|
||||
}
|
||||
|
||||
public long getLastMessageTime() {
|
||||
return lastMessageTime;
|
||||
}
|
||||
|
||||
public long getLastVoiceJoin() {
|
||||
return lastVoiceJoin;
|
||||
}
|
||||
|
||||
// Setters (для Gson)
|
||||
public void setLevel(int level) {
|
||||
this.level = level;
|
||||
}
|
||||
|
||||
public void setTotalXP(int totalXP) {
|
||||
this.totalXP = totalXP;
|
||||
}
|
||||
|
||||
public void setLastMessageTime(long lastMessageTime) {
|
||||
this.lastMessageTime = lastMessageTime;
|
||||
}
|
||||
|
||||
public void setLastVoiceJoin(long lastVoiceJoin) {
|
||||
this.lastVoiceJoin = lastVoiceJoin;
|
||||
}
|
||||
|
||||
// Методы для изменения статистики
|
||||
public void addXP(int xp) {
|
||||
this.totalXP += xp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Записать сообщение с временной меткой
|
||||
* Переименовано из incrementMessages() для явности side effect
|
||||
*/
|
||||
public void recordMessage(long timestamp) {
|
||||
this.messageCount++;
|
||||
this.lastMessageTime = timestamp;
|
||||
}
|
||||
|
||||
public void addVoiceMinutes(long minutes) {
|
||||
this.voiceMinutes += minutes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
UserStats userStats = (UserStats) o;
|
||||
return Objects.equals(userId, userStats.userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("UserStats[%s: Lvl %d, XP %d, Msgs %d, Voice %dm]",
|
||||
userId, level, totalXP, messageCount, voiceMinutes);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
package creator.loki.feature.level.domain;
|
||||
|
||||
/**
|
||||
* Формула расчета опыта и уровней
|
||||
* Использует линейную прогрессию: 200 XP на уровень
|
||||
*/
|
||||
public class XpFormula {
|
||||
private static final int XP_PER_LEVEL = 200;
|
||||
|
||||
/**
|
||||
* Вычисляет общее количество XP необходимое для достижения уровня
|
||||
* Формула: 200 * n * (n+1) / 2
|
||||
*
|
||||
* @param level целевой уровень
|
||||
* @return общее количество XP
|
||||
*/
|
||||
public int getTotalXPForLevel(int level) {
|
||||
if (level <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return XP_PER_LEVEL * level * (level + 1) / 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Вычисляет уровень по количеству XP
|
||||
* Обратная формула с использованием квадратного уравнения
|
||||
*
|
||||
* @param xp текущее количество XP
|
||||
* @return текущий уровень
|
||||
*/
|
||||
public int calculateLevel(int xp) {
|
||||
if (xp <= 0) {
|
||||
return 0;
|
||||
}
|
||||
double discriminant = 1 + 8.0 * xp / XP_PER_LEVEL;
|
||||
return (int) Math.floor((-1 + Math.sqrt(discriminant)) / 2.0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Вычисляет XP необходимое для следующего уровня
|
||||
*
|
||||
* @param currentLevel текущий уровень
|
||||
* @return XP для следующего уровня
|
||||
*/
|
||||
public int getXpForNextLevel(int currentLevel) {
|
||||
return XP_PER_LEVEL * (currentLevel + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Вычисляет прогресс до следующего уровня
|
||||
*
|
||||
* @param currentXp текущее количество XP
|
||||
* @param currentLevel текущий уровень
|
||||
* @return прогресс от 0.0 до 1.0
|
||||
*/
|
||||
public double getProgressToNextLevel(int currentXp, int currentLevel) {
|
||||
int xpForCurrentLevel = getTotalXPForLevel(currentLevel);
|
||||
int xpForNextLevel = getTotalXPForLevel(currentLevel + 1);
|
||||
int xpNeeded = xpForNextLevel - xpForCurrentLevel;
|
||||
int xpProgress = currentXp - xpForCurrentLevel;
|
||||
|
||||
if (xpNeeded <= 0) {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
return Math.min(1.0, Math.max(0.0, (double) xpProgress / xpNeeded));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
package creator.loki.feature.level.service;
|
||||
|
||||
import creator.loki.feature.level.domain.UserStats;
|
||||
import creator.loki.repository.UserStatsRepository;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Сервис для работы с таблицами лидеров
|
||||
*/
|
||||
public class LeaderboardService {
|
||||
private final UserStatsRepository repository;
|
||||
|
||||
public LeaderboardService(UserStatsRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить топ пользователей по уровню
|
||||
*/
|
||||
public List<Map.Entry<String, UserStats>> getTopByLevel(int limit) {
|
||||
return getTop(limit, Comparator.comparingInt(stats -> -stats.getLevel()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить топ пользователей по количеству сообщений
|
||||
*/
|
||||
public List<Map.Entry<String, UserStats>> getTopByMessages(int limit) {
|
||||
return getTop(limit, Comparator.comparingInt(stats -> -stats.getMessageCount()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить топ пользователей по времени в войсе
|
||||
*/
|
||||
public List<Map.Entry<String, UserStats>> getTopByVoice(int limit) {
|
||||
return getTop(limit, Comparator.comparingLong(stats -> -stats.getVoiceMinutes()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить позицию пользователя в рейтинге по уровню
|
||||
*/
|
||||
public int getUserRankByLevel(String userId) {
|
||||
List<Map.Entry<String, UserStats>> top = getTopByLevel(Integer.MAX_VALUE);
|
||||
for (int i = 0; i < top.size(); i++) {
|
||||
if (top.get(i).getKey().equals(userId)) {
|
||||
return i + 1;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Обобщенный метод получения топа с кастомным компаратором
|
||||
*/
|
||||
private List<Map.Entry<String, UserStats>> getTop(int limit, Comparator<UserStats> comparator) {
|
||||
Map<String, UserStats> allStats = repository.getAllStats();
|
||||
|
||||
return allStats.entrySet().stream()
|
||||
.sorted((e1, e2) -> comparator.compare(e1.getValue(), e2.getValue()))
|
||||
.limit(limit)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
package creator.loki.feature.level.service;
|
||||
|
||||
import creator.loki.feature.level.domain.LevelProgress;
|
||||
import creator.loki.feature.level.domain.LevelUpResult;
|
||||
import creator.loki.feature.level.domain.UserStats;
|
||||
import creator.loki.feature.level.domain.XpFormula;
|
||||
import creator.loki.repository.UserStatsRepository;
|
||||
|
||||
/**
|
||||
* Сервис для управления системой уровней
|
||||
* Рефакторинг LevelSystem - только бизнес-логика начисления XP
|
||||
*/
|
||||
public class LevelService {
|
||||
private static final int XP_PER_MESSAGE = 10;
|
||||
private static final int XP_PER_VOICE_MINUTE = 1;
|
||||
|
||||
private final UserStatsRepository repository;
|
||||
private final XpFormula xpFormula;
|
||||
private final MessageCooldownService cooldownService;
|
||||
|
||||
public LevelService(
|
||||
UserStatsRepository repository,
|
||||
XpFormula xpFormula,
|
||||
MessageCooldownService cooldownService
|
||||
) {
|
||||
this.repository = repository;
|
||||
this.xpFormula = xpFormula;
|
||||
this.cooldownService = cooldownService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Добавить XP за сообщение с проверкой кулдауна
|
||||
*
|
||||
* @param userId ID пользователя
|
||||
* @return результат повышения уровня или null
|
||||
*/
|
||||
public LevelUpResult addMessageXP(String userId) {
|
||||
if (!cooldownService.canReceiveXP(userId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
UserStats stats = repository.getUserStats(userId);
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
stats.recordMessage(now);
|
||||
|
||||
int oldLevel = stats.getLevel();
|
||||
stats.addXP(XP_PER_MESSAGE);
|
||||
|
||||
int newLevel = xpFormula.calculateLevel(stats.getTotalXP());
|
||||
stats.setLevel(newLevel);
|
||||
|
||||
repository.saveUserStats(userId, stats);
|
||||
|
||||
if (newLevel > oldLevel) {
|
||||
return new LevelUpResult(oldLevel, newLevel, stats.getTotalXP());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Добавить XP за время в войсе
|
||||
*
|
||||
* @param userId ID пользователя
|
||||
* @param minutes количество минут
|
||||
* @return результат повышения уровня или null
|
||||
*/
|
||||
public LevelUpResult addVoiceXP(String userId, long minutes) {
|
||||
UserStats stats = repository.getUserStats(userId);
|
||||
stats.addVoiceMinutes(minutes);
|
||||
|
||||
int oldLevel = stats.getLevel();
|
||||
stats.addXP((int)(minutes * XP_PER_VOICE_MINUTE));
|
||||
|
||||
int newLevel = xpFormula.calculateLevel(stats.getTotalXP());
|
||||
stats.setLevel(newLevel);
|
||||
|
||||
repository.saveUserStats(userId, stats);
|
||||
|
||||
if (newLevel > oldLevel) {
|
||||
return new LevelUpResult(oldLevel, newLevel, stats.getTotalXP());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить прогресс пользователя до следующего уровня
|
||||
*/
|
||||
public LevelProgress getProgress(String userId) {
|
||||
UserStats stats = repository.getUserStats(userId);
|
||||
int currentLevel = stats.getLevel();
|
||||
int currentXP = stats.getTotalXP();
|
||||
|
||||
int xpForCurrentLevel = xpFormula.getTotalXPForLevel(currentLevel);
|
||||
int xpForNextLevel = xpFormula.getTotalXPForLevel(currentLevel + 1);
|
||||
|
||||
int xpInCurrentLevel = currentXP - xpForCurrentLevel;
|
||||
int xpNeededForNext = xpForNextLevel - xpForCurrentLevel;
|
||||
|
||||
double percentage = xpFormula.getProgressToNextLevel(currentXP, currentLevel) * 100;
|
||||
|
||||
return new LevelProgress(
|
||||
currentLevel,
|
||||
currentXP,
|
||||
xpInCurrentLevel,
|
||||
xpNeededForNext,
|
||||
percentage
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить статистику пользователя
|
||||
*/
|
||||
public UserStats getUserStats(String userId) {
|
||||
return repository.getUserStats(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить XP необходимый для следующего уровня
|
||||
*/
|
||||
public int getXPForNextLevel(int currentLevel) {
|
||||
return xpFormula.getXpForNextLevel(currentLevel);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package creator.loki.feature.level.service;
|
||||
|
||||
import creator.loki.feature.level.domain.UserStats;
|
||||
import creator.loki.repository.UserStatsRepository;
|
||||
|
||||
/**
|
||||
* Сервис для управления кулдауном сообщений
|
||||
* Использует персистентное хранение через UserStats.lastMessageTime
|
||||
*/
|
||||
public class MessageCooldownService {
|
||||
private static final long MESSAGE_COOLDOWN_MS = 60000; // 1 минута
|
||||
|
||||
private final UserStatsRepository repository;
|
||||
|
||||
public MessageCooldownService(UserStatsRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить, может ли пользователь получить XP за сообщение
|
||||
*
|
||||
* @param userId ID пользователя
|
||||
* @return true если кулдаун прошел
|
||||
*/
|
||||
public boolean canReceiveXP(String userId) {
|
||||
UserStats stats = repository.getUserStats(userId);
|
||||
long now = System.currentTimeMillis();
|
||||
long lastMessage = stats.getLastMessageTime();
|
||||
|
||||
return lastMessage == 0 || (now - lastMessage) >= MESSAGE_COOLDOWN_MS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить оставшееся время кулдауна в секундах
|
||||
*
|
||||
* @param userId ID пользователя
|
||||
* @return секунды до окончания кулдауна, 0 если кулдаун прошел
|
||||
*/
|
||||
public long getRemainingCooldown(String userId) {
|
||||
UserStats stats = repository.getUserStats(userId);
|
||||
long now = System.currentTimeMillis();
|
||||
long lastMessage = stats.getLastMessageTime();
|
||||
|
||||
if (lastMessage == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
long elapsed = now - lastMessage;
|
||||
if (elapsed >= MESSAGE_COOLDOWN_MS) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (MESSAGE_COOLDOWN_MS - elapsed) / 1000;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
package creator.loki.feature.profile;
|
||||
|
||||
import creator.loki.command.Command;
|
||||
import creator.loki.command.CommandContext;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
import net.dv8tion.jda.api.utils.FileUpload;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Команда для просмотра профиля с красивой карточкой
|
||||
*/
|
||||
public class ProfileCommand implements Command {
|
||||
private static final Logger logger = LoggerFactory.getLogger(ProfileCommand.class);
|
||||
private final ProfileService profileService;
|
||||
|
||||
public ProfileCommand(ProfileService profileService) {
|
||||
this.profileService = profileService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "profile";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "Показать красивую профильную карточку";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsage() {
|
||||
return "profile [@user]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCategory() {
|
||||
return "Статистика";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(CommandContext context) {
|
||||
Member target;
|
||||
|
||||
if (!context.getMessage().getMentions().getMembers().isEmpty()) {
|
||||
target = context.getMessage().getMentions().getMembers().get(0);
|
||||
} else {
|
||||
target = context.getAuthor();
|
||||
}
|
||||
|
||||
if (target == null) {
|
||||
context.getChannel().sendMessage("❌ Пользователь не найден!").queue();
|
||||
return;
|
||||
}
|
||||
|
||||
// Отправляем сообщение о генерации
|
||||
context.getChannel().sendMessage("🎨 Генерирую профильную карточку...").queue(msg -> {
|
||||
try {
|
||||
// Генерируем карточку
|
||||
byte[] imageBytes = profileService.generateProfileCard(target);
|
||||
|
||||
// Отправляем изображение
|
||||
context.getChannel().sendFiles(
|
||||
FileUpload.fromData(imageBytes, "profile.png")
|
||||
).queue(
|
||||
success -> {
|
||||
msg.delete().queue();
|
||||
logger.info("Профильная карточка сгенерирована для {}", target.getEffectiveName());
|
||||
},
|
||||
error -> {
|
||||
msg.editMessage("❌ Ошибка при отправке карточки!").queue();
|
||||
logger.error("Ошибка отправки профильной карточки", error);
|
||||
}
|
||||
);
|
||||
} catch (Exception e) {
|
||||
msg.editMessage("❌ Ошибка при генерации карточки: " + e.getMessage()).queue();
|
||||
logger.error("Ошибка генерации профильной карточки", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
53
src/main/java/creator/loki/feature/profile/ProfileData.java
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
package creator.loki.feature.profile;
|
||||
|
||||
/**
|
||||
* Данные для генерации профильной карточки
|
||||
*/
|
||||
public class ProfileData {
|
||||
private String username;
|
||||
private String avatarUrl;
|
||||
private String subtitle;
|
||||
private String tag;
|
||||
private int level;
|
||||
private int xp;
|
||||
private int maxXp;
|
||||
private int rank;
|
||||
private int messages;
|
||||
private int reputation;
|
||||
private String activity;
|
||||
private String background;
|
||||
private String badge;
|
||||
|
||||
public ProfileData(String username, String avatarUrl, String subtitle, String tag,
|
||||
int level, int xp, int maxXp, int rank, int messages,
|
||||
int reputation, String activity, String background, String badge) {
|
||||
this.username = username;
|
||||
this.avatarUrl = avatarUrl;
|
||||
this.subtitle = subtitle;
|
||||
this.tag = tag;
|
||||
this.level = level;
|
||||
this.xp = xp;
|
||||
this.maxXp = maxXp;
|
||||
this.rank = rank;
|
||||
this.messages = messages;
|
||||
this.reputation = reputation;
|
||||
this.activity = activity;
|
||||
this.background = background;
|
||||
this.badge = badge;
|
||||
}
|
||||
|
||||
// Getters
|
||||
public String getUsername() { return username; }
|
||||
public String getAvatarUrl() { return avatarUrl; }
|
||||
public String getSubtitle() { return subtitle; }
|
||||
public String getTag() { return tag; }
|
||||
public int getLevel() { return level; }
|
||||
public int getXp() { return xp; }
|
||||
public int getMaxXp() { return maxXp; }
|
||||
public int getRank() { return rank; }
|
||||
public int getMessages() { return messages; }
|
||||
public int getReputation() { return reputation; }
|
||||
public String getActivity() { return activity; }
|
||||
public String getBackground() { return background; }
|
||||
public String getBadge() { return badge; }
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
package creator.loki.feature.profile;
|
||||
|
||||
import creator.loki.feature.level.domain.UserStats;
|
||||
import creator.loki.feature.level.service.LeaderboardService;
|
||||
import creator.loki.feature.level.service.LevelService;
|
||||
import creator.loki.repository.UserStatsRepository;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
/**
|
||||
* Сервис для генерации профильных карточек
|
||||
*/
|
||||
public class ProfileService {
|
||||
private final UserStatsRepository repository;
|
||||
private final LevelService levelService;
|
||||
private final LeaderboardService leaderboardService;
|
||||
private final RankCardGenerator generator;
|
||||
|
||||
public ProfileService(
|
||||
UserStatsRepository repository,
|
||||
LevelService levelService,
|
||||
LeaderboardService leaderboardService
|
||||
) {
|
||||
this.repository = repository;
|
||||
this.levelService = levelService;
|
||||
this.leaderboardService = leaderboardService;
|
||||
this.generator = new RankCardGenerator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Сгенерировать профильную карточку для пользователя
|
||||
*/
|
||||
public byte[] generateProfileCard(Member member) throws Exception {
|
||||
String userId = member.getId();
|
||||
UserStats stats = repository.getUserStats(userId);
|
||||
|
||||
// Получаем ранг пользователя
|
||||
int rank = leaderboardService.getUserRankByLevel(userId);
|
||||
if (rank == -1) rank = 999;
|
||||
|
||||
// Получаем прогресс
|
||||
var progress = levelService.getProgress(userId);
|
||||
|
||||
// Вычисляем активность (часы в войсе)
|
||||
long hours = stats.getVoiceMinutes() / 60;
|
||||
long minutes = stats.getVoiceMinutes() % 60;
|
||||
String activity = hours + "ч " + minutes + "м";
|
||||
|
||||
// Получаем репутацию (пока заглушка, потом добавим)
|
||||
int reputation = 0; // TODO: добавить систему репутации
|
||||
|
||||
// Определяем бейдж по уровню
|
||||
String badge = getBadgeForLevel(stats.getLevel());
|
||||
|
||||
// Определяем фон
|
||||
String background = "fon1.png";
|
||||
|
||||
// Создаем данные профиля
|
||||
ProfileData data = new ProfileData(
|
||||
member.getEffectiveName(),
|
||||
member.getEffectiveAvatarUrl() + "?size=256",
|
||||
"Участник сервера",
|
||||
"LVL",
|
||||
stats.getLevel(),
|
||||
progress.getCurrentLevelXP(),
|
||||
progress.getXpForNextLevel(),
|
||||
rank,
|
||||
stats.getMessageCount(),
|
||||
reputation,
|
||||
activity,
|
||||
background,
|
||||
badge
|
||||
);
|
||||
|
||||
// Генерируем изображение
|
||||
BufferedImage image = generator.generate(data);
|
||||
|
||||
// Конвертируем в байты
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
ImageIO.write(image, "PNG", baos);
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Определить бейдж по уровню
|
||||
*/
|
||||
private String getBadgeForLevel(int level) {
|
||||
if (level >= 50) return "shield-outline-badged-svgrepo-com.svg";
|
||||
if (level >= 30) return "shield-alt-1-svgrepo-com.svg";
|
||||
if (level >= 20) return "shield-outline-badged-svgrepo-com.svg";
|
||||
if (level >= 10) return "shield-alt-1-svgrepo-com.svg";
|
||||
if (level >= 5) return "shield-outline-badged-svgrepo-com.svg";
|
||||
return "shield-alt-1-svgrepo-com.svg";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
package creator.loki.feature.profile;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.*;
|
||||
import java.awt.geom.Ellipse2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
|
||||
/**
|
||||
* Генератор красивых профильных карточек
|
||||
*/
|
||||
public class RankCardGenerator {
|
||||
private static final int WIDTH = 1400;
|
||||
private static final int HEIGHT = 500;
|
||||
|
||||
public BufferedImage generate(ProfileData data) throws Exception {
|
||||
BufferedImage canvas = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g = canvas.createGraphics();
|
||||
|
||||
// ===== QUALITY =====
|
||||
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||
|
||||
// ===== BACKGROUND =====
|
||||
BufferedImage bg = loadResource("backgrounds/" + data.getBackground());
|
||||
g.drawImage(bg, 0, 0, WIDTH, HEIGHT, null);
|
||||
|
||||
// ===== DARK OVERLAY =====
|
||||
g.setColor(new Color(0, 0, 0, 170));
|
||||
g.fillRoundRect(20, 20, WIDTH - 40, HEIGHT - 40, 40, 40);
|
||||
|
||||
// ===== ORANGE BORDER =====
|
||||
g.setStroke(new BasicStroke(2f));
|
||||
g.setColor(new Color(255, 140, 0, 100));
|
||||
g.drawRoundRect(20, 20, WIDTH - 40, HEIGHT - 40, 40, 40);
|
||||
|
||||
// ===== AVATAR =====
|
||||
BufferedImage avatar = ImageIO.read(new URL(data.getAvatarUrl()));
|
||||
drawCircleImage(g, avatar, 70, 90, 180);
|
||||
|
||||
// ===== AVATAR GLOW =====
|
||||
g.setStroke(new BasicStroke(5f));
|
||||
g.setColor(new Color(255, 140, 0));
|
||||
g.drawOval(70, 90, 180, 180);
|
||||
|
||||
// ===== USERNAME =====
|
||||
g.setFont(loadFont("fonts/Inter-Bold.ttf", 52f));
|
||||
g.setColor(Color.WHITE);
|
||||
g.drawString(data.getUsername(), 320, 150);
|
||||
|
||||
// ===== SMALL TAG =====
|
||||
g.setFont(loadFont("fonts/Inter-Bold.ttf", 24f));
|
||||
g.setColor(new Color(255, 140, 0));
|
||||
g.fillRoundRect(670, 110, 90, 42, 15, 15);
|
||||
g.setColor(Color.BLACK);
|
||||
g.drawString(data.getTag(), 690, 138);
|
||||
|
||||
// ===== SUBTITLE =====
|
||||
g.setFont(loadFont("fonts/Inter-Regular.ttf", 28f));
|
||||
g.setColor(new Color(220, 220, 220));
|
||||
g.drawString(data.getSubtitle(), 320, 210);
|
||||
|
||||
// ===== LEVEL =====
|
||||
g.setFont(loadFont("fonts/Inter-Bold.ttf", 48f));
|
||||
g.setColor(new Color(255, 140, 0));
|
||||
g.drawString(String.valueOf(data.getLevel()), 320, 300);
|
||||
g.setFont(loadFont("fonts/Inter-Bold.ttf", 36f));
|
||||
g.drawString("УРОВЕНЬ", 380, 300);
|
||||
|
||||
// ===== XP TEXT =====
|
||||
g.setFont(loadFont("fonts/Inter-Regular.ttf", 34f));
|
||||
g.setColor(Color.WHITE);
|
||||
g.drawString(data.getXp() + " / " + data.getMaxXp() + " XP", 920, 300);
|
||||
|
||||
// ===== XP BAR =====
|
||||
int barX = 320;
|
||||
int barY = 340;
|
||||
int barWidth = 620;
|
||||
int barHeight = 28;
|
||||
g.setColor(new Color(255, 255, 255, 40));
|
||||
g.fillRoundRect(barX, barY, barWidth, barHeight, 20, 20);
|
||||
|
||||
float progress = (float) data.getXp() / data.getMaxXp();
|
||||
int fill = (int) (barWidth * progress);
|
||||
g.setColor(new Color(255, 140, 0));
|
||||
g.fillRoundRect(barX, barY, fill, barHeight, 20, 20);
|
||||
|
||||
// ===== XP GLOW DOT =====
|
||||
int dotX = barX + fill - 10;
|
||||
g.setColor(Color.WHITE);
|
||||
g.fillOval(dotX, barY - 6, 40, 40);
|
||||
|
||||
// ===== BADGE =====
|
||||
try {
|
||||
BufferedImage badge = loadResource("badges/" + data.getBadge());
|
||||
g.drawImage(badge, 1080, 90, 200, 200, null);
|
||||
} catch (Exception e) {
|
||||
// Если бейдж не загрузился, рисуем заглушку
|
||||
g.setColor(new Color(255, 140, 0));
|
||||
g.fillOval(1080, 90, 200, 200);
|
||||
g.setColor(Color.WHITE);
|
||||
g.setFont(loadFont("fonts/Inter-Bold.ttf", 48f));
|
||||
g.drawString("★", 1150, 200);
|
||||
}
|
||||
|
||||
// ===== RANK TEXT =====
|
||||
g.setFont(loadFont("fonts/Inter-Bold.ttf", 34f));
|
||||
g.setColor(new Color(255, 140, 0));
|
||||
g.drawString("РАНГ", 1130, 340);
|
||||
g.setFont(loadFont("fonts/Inter-Bold.ttf", 60f));
|
||||
g.setColor(Color.WHITE);
|
||||
g.drawString("#" + data.getRank(), 1110, 400);
|
||||
|
||||
// ===== STATS =====
|
||||
drawStat(g, "АКТИВНОСТЬ", data.getActivity(), "icons/clock-five-thirty-svgrepo-com.svg", 80, 400);
|
||||
drawStat(g, "СООБЩЕНИЙ", String.valueOf(data.getMessages()), "icons/message-square-lines-svgrepo-com.svg", 500, 400);
|
||||
drawStat(g, "РЕПУТАЦИЯ", String.valueOf(data.getReputation()), "icons/star-svgrepo-com.svg", 920, 400);
|
||||
|
||||
g.dispose();
|
||||
return canvas;
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
private void drawStat(Graphics2D g, String title, String value, String iconPath, int x, int y) throws Exception {
|
||||
g.setColor(new Color(15, 15, 15, 180));
|
||||
g.fillRoundRect(x, y, 340, 110, 30, 30);
|
||||
|
||||
try {
|
||||
BufferedImage icon = loadResource(iconPath);
|
||||
g.drawImage(icon, x + 25, y + 25, 48, 48, null);
|
||||
} catch (Exception e) {
|
||||
// Если иконка не загрузилась, рисуем заглушку
|
||||
g.setColor(new Color(255, 140, 0));
|
||||
g.fillOval(x + 25, y + 25, 48, 48);
|
||||
}
|
||||
|
||||
g.setFont(loadFont("fonts/Inter-Bold.ttf", 36f));
|
||||
g.setColor(Color.WHITE);
|
||||
g.drawString(value, x + 100, y + 50);
|
||||
|
||||
g.setFont(loadFont("fonts/Inter-Regular.ttf", 24f));
|
||||
g.setColor(new Color(180, 180, 180));
|
||||
g.drawString(title, x + 100, y + 90);
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
private void drawCircleImage(Graphics2D g, BufferedImage image, int x, int y, int size) {
|
||||
Ellipse2D.Double circle = new Ellipse2D.Double(x, y, size, size);
|
||||
g.setClip(circle);
|
||||
g.drawImage(image, x, y, size, size, null);
|
||||
g.setClip(null);
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
private Font loadFont(String path, float size) throws Exception {
|
||||
InputStream is = getClass().getClassLoader().getResourceAsStream("assets/" + path);
|
||||
if (is == null) {
|
||||
// Fallback на системный шрифт
|
||||
return new Font("Arial", Font.PLAIN, (int) size);
|
||||
}
|
||||
return Font.createFont(Font.TRUETYPE_FONT, is).deriveFont(size);
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
private BufferedImage loadResource(String path) throws Exception {
|
||||
InputStream is = getClass().getClassLoader().getResourceAsStream("assets/" + path);
|
||||
if (is == null) {
|
||||
throw new IllegalArgumentException("Resource not found: assets/" + path);
|
||||
}
|
||||
return ImageIO.read(is);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
package creator.loki.level;
|
||||
|
||||
import creator.loki.data.JsonDataStore;
|
||||
import creator.loki.feature.level.domain.LevelProgress;
|
||||
import creator.loki.feature.level.domain.LevelUpResult;
|
||||
import creator.loki.feature.level.domain.UserStats;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.*;
|
||||
|
|
@ -13,7 +14,6 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||
* Система уровней и опыта
|
||||
*/
|
||||
public class LevelSystem {
|
||||
private static final Logger logger = LoggerFactory.getLogger(LevelSystem.class);
|
||||
private final JsonDataStore dataStore;
|
||||
|
||||
// XP за действия
|
||||
|
|
@ -73,7 +73,7 @@ public class LevelSystem {
|
|||
messageTimestamps.put(userId, now);
|
||||
|
||||
UserStats stats = getUserStats(userId);
|
||||
stats.incrementMessages();
|
||||
stats.recordMessage(now);
|
||||
|
||||
int oldLevel = stats.getLevel();
|
||||
stats.addXP(XP_PER_MESSAGE);
|
||||
|
|
@ -190,48 +190,4 @@ public class LevelSystem {
|
|||
.limit(limit)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Результат повышения уровня
|
||||
*/
|
||||
public static class LevelUpResult {
|
||||
private final int oldLevel;
|
||||
private final int newLevel;
|
||||
private final int totalXP;
|
||||
|
||||
public LevelUpResult(int oldLevel, int newLevel, int totalXP) {
|
||||
this.oldLevel = oldLevel;
|
||||
this.newLevel = newLevel;
|
||||
this.totalXP = totalXP;
|
||||
}
|
||||
|
||||
public int getOldLevel() { return oldLevel; }
|
||||
public int getNewLevel() { return newLevel; }
|
||||
public int getTotalXP() { return totalXP; }
|
||||
}
|
||||
|
||||
/**
|
||||
* Прогресс уровня
|
||||
*/
|
||||
public static class LevelProgress {
|
||||
private final int level;
|
||||
private final int totalXP;
|
||||
private final int currentLevelXP;
|
||||
private final int xpForNextLevel;
|
||||
private final double percentage;
|
||||
|
||||
public LevelProgress(int level, int totalXP, int currentLevelXP, int xpForNextLevel, double percentage) {
|
||||
this.level = level;
|
||||
this.totalXP = totalXP;
|
||||
this.currentLevelXP = currentLevelXP;
|
||||
this.xpForNextLevel = xpForNextLevel;
|
||||
this.percentage = percentage;
|
||||
}
|
||||
|
||||
public int getLevel() { return level; }
|
||||
public int getTotalXP() { return totalXP; }
|
||||
public int getCurrentLevelXP() { return currentLevelXP; }
|
||||
public int getXpForNextLevel() { return xpForNextLevel; }
|
||||
public double getPercentage() { return percentage; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,53 +0,0 @@
|
|||
package creator.loki.level;
|
||||
|
||||
/**
|
||||
* Статистика пользователя
|
||||
*/
|
||||
public class UserStats {
|
||||
private String userId;
|
||||
private int level;
|
||||
private int totalXP;
|
||||
private int messageCount;
|
||||
private long voiceMinutes;
|
||||
private long lastMessageTime;
|
||||
private long lastVoiceJoin;
|
||||
|
||||
public UserStats(String userId) {
|
||||
this.userId = userId;
|
||||
this.level = 0;
|
||||
this.totalXP = 0;
|
||||
this.messageCount = 0;
|
||||
this.voiceMinutes = 0;
|
||||
this.lastMessageTime = 0;
|
||||
this.lastVoiceJoin = 0;
|
||||
}
|
||||
|
||||
// Getters
|
||||
public String getUserId() { return userId; }
|
||||
public int getLevel() { return level; }
|
||||
public int getTotalXP() { return totalXP; }
|
||||
public int getMessageCount() { return messageCount; }
|
||||
public long getVoiceMinutes() { return voiceMinutes; }
|
||||
public long getLastMessageTime() { return lastMessageTime; }
|
||||
public long getLastVoiceJoin() { return lastVoiceJoin; }
|
||||
|
||||
// Setters
|
||||
public void setLevel(int level) { this.level = level; }
|
||||
public void setTotalXP(int totalXP) { this.totalXP = totalXP; }
|
||||
public void setLastMessageTime(long lastMessageTime) { this.lastMessageTime = lastMessageTime; }
|
||||
public void setLastVoiceJoin(long lastVoiceJoin) { this.lastVoiceJoin = lastVoiceJoin; }
|
||||
|
||||
// Методы для изменения статистики
|
||||
public void addXP(int xp) {
|
||||
this.totalXP += xp;
|
||||
}
|
||||
|
||||
public void incrementMessages() {
|
||||
this.messageCount++;
|
||||
this.lastMessageTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public void addVoiceMinutes(long minutes) {
|
||||
this.voiceMinutes += minutes;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package creator.loki.listener;
|
||||
|
||||
import creator.loki.database.ConfigRepository;
|
||||
import creator.loki.feature.level.domain.LevelProgress;
|
||||
import creator.loki.level.LevelSystem;
|
||||
import net.dv8tion.jda.api.EmbedBuilder;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
|
|
@ -28,7 +29,7 @@ public class LevelUpNotifier {
|
|||
* Отправить уведомление о повышении уровня
|
||||
*/
|
||||
public void sendLevelUpNotification(Member member, int oldLevel, int newLevel, MessageChannel channel) {
|
||||
LevelSystem.LevelProgress progress = levelSystem.getProgress(member.getId());
|
||||
LevelProgress progress = levelSystem.getProgress(member.getId());
|
||||
|
||||
EmbedBuilder embed = new EmbedBuilder()
|
||||
.setTitle("🎉 Повышение уровня!")
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import com.google.gson.reflect.TypeToken;
|
|||
import creator.loki.core.EventDispatcher;
|
||||
import creator.loki.data.JsonDataStore;
|
||||
import creator.loki.embed.EmbedTemplates;
|
||||
import creator.loki.feature.level.domain.LevelUpResult;
|
||||
import creator.loki.level.LevelSystem;
|
||||
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
|
||||
import net.dv8tion.jda.api.hooks.ListenerAdapter;
|
||||
|
|
@ -32,7 +33,7 @@ public class MessageListener extends ListenerAdapter {
|
|||
String userId = event.getAuthor().getId();
|
||||
|
||||
// Начисляем XP и проверяем повышение уровня
|
||||
LevelSystem.LevelUpResult levelUp = levelSystem.addMessageXP(userId);
|
||||
LevelUpResult levelUp = levelSystem.addMessageXP(userId);
|
||||
if (levelUp != null) {
|
||||
// Отправляем сообщение о повышении уровня
|
||||
event.getChannel().sendMessageEmbeds(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package creator.loki.repository;
|
||||
|
||||
import creator.loki.level.UserStats;
|
||||
import creator.loki.feature.level.domain.UserStats;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
|
@ -29,4 +29,25 @@ public interface UserStatsRepository {
|
|||
* Проверить существование пользователя
|
||||
*/
|
||||
boolean exists(String userId);
|
||||
|
||||
/**
|
||||
* Получить статистику пользователя или создать новую
|
||||
*/
|
||||
default UserStats getUserStats(String userId) {
|
||||
return findById(userId).orElse(new UserStats(userId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Сохранить статистику пользователя (алиас для save)
|
||||
*/
|
||||
default void saveUserStats(String userId, UserStats stats) {
|
||||
save(stats);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить всю статистику (алиас для findAll)
|
||||
*/
|
||||
default Map<String, UserStats> getAllStats() {
|
||||
return findAll();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package creator.loki.repository.impl;
|
|||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import creator.loki.data.JsonDataStore;
|
||||
import creator.loki.level.UserStats;
|
||||
import creator.loki.feature.level.domain.UserStats;
|
||||
import creator.loki.repository.UserStatsRepository;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
|
|
|
|||
42
src/main/resources/assets/README.md
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# Assets для профильных карточек
|
||||
|
||||
## Структура
|
||||
|
||||
### backgrounds/
|
||||
Фоновые изображения для карточек (1400x500px)
|
||||
- `default.jpg` - дефолтный фон
|
||||
|
||||
### badges/
|
||||
Бейджи по уровням (200x200px)
|
||||
- `newbie.png` - уровень 0-4
|
||||
- `active.png` - уровень 5-9
|
||||
- `regular.png` - уровень 10-19
|
||||
- `veteran.png` - уровень 20-29
|
||||
- `legend.png` - уровень 30-49
|
||||
- `master.png` - уровень 50+
|
||||
|
||||
### icons/
|
||||
Иконки для статистики (48x48px)
|
||||
- `clock.png` - активность (время в войсе)
|
||||
- `chat.png` - сообщения
|
||||
- `star.png` - репутация
|
||||
|
||||
### fonts/
|
||||
Шрифты для текста
|
||||
- `Inter-Bold.ttf` - жирный шрифт
|
||||
- `Inter-Regular.ttf` - обычный шрифт
|
||||
|
||||
## Где скачать шрифты Inter
|
||||
|
||||
https://fonts.google.com/specimen/Inter
|
||||
|
||||
Скачайте и положите файлы:
|
||||
- Inter-Bold.ttf
|
||||
- Inter-Regular.ttf
|
||||
|
||||
в папку `src/main/resources/assets/fonts/`
|
||||
|
||||
## Примеры изображений
|
||||
|
||||
Можно использовать любые изображения подходящего размера.
|
||||
Для тестирования можно создать простые цветные заглушки.
|
||||
BIN
src/main/resources/assets/backgrounds/fon1.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
2
src/main/resources/assets/badges/.gitkeep
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
# Placeholder для бейджей
|
||||
# Добавьте PNG изображения бейджей (200x200px)
|
||||
2
src/main/resources/assets/fonts/.gitkeep
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
# Placeholder для шрифтов
|
||||
# Скачайте Inter-Bold.ttf и Inter-Regular.ttf с https://fonts.google.com/specimen/Inter
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 17V12L13.5 14.5M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 453 B |
14
src/main/resources/assets/icons/lightning-svgrepo-com.svg
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg height="800px" width="800px" version="1.1" id="_x32_" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 512 512" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#000000;}
|
||||
</style>
|
||||
<g>
|
||||
<polygon class="st0" points="386.415,193.208 287.481,193.208 359.434,0 161.566,0 125.585,280.151 206.528,280.151 170.557,512
|
||||
"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 608 B |
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7 9H17M7 13H12M21 20L17.6757 18.3378C17.4237 18.2118 17.2977 18.1488 17.1656 18.1044C17.0484 18.065 16.9277 18.0365 16.8052 18.0193C16.6672 18 16.5263 18 16.2446 18H6.2C5.07989 18 4.51984 18 4.09202 17.782C3.71569 17.5903 3.40973 17.2843 3.21799 16.908C3 16.4802 3 15.9201 3 14.8V7.2C3 6.07989 3 5.51984 3.21799 5.09202C3.40973 4.71569 3.71569 4.40973 4.09202 4.21799C4.51984 4 5.0799 4 6.2 4H17.8C18.9201 4 19.4802 4 19.908 4.21799C20.2843 4.40973 20.5903 4.71569 20.782 5.09202C21 5.51984 21 6.0799 21 7.2V20Z" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 833 B |
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.302 21.6149C11.5234 21.744 11.6341 21.8086 11.7903 21.8421C11.9116 21.8681 12.0884 21.8681 12.2097 21.8421C12.3659 21.8086 12.4766 21.744 12.698 21.6149C14.646 20.4784 20 16.9084 20 12V6.6C20 6.04207 20 5.7631 19.8926 5.55048C19.7974 5.36198 19.6487 5.21152 19.4613 5.11409C19.25 5.00419 18.9663 5.00084 18.3988 4.99413C15.4272 4.95899 13.7136 4.71361 12 3C10.2864 4.71361 8.57279 4.95899 5.6012 4.99413C5.03373 5.00084 4.74999 5.00419 4.53865 5.11409C4.35129 5.21152 4.20259 5.36198 4.10739 5.55048C4 5.7631 4 6.04207 4 6.6V12C4 16.9084 9.35396 20.4784 11.302 21.6149Z" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 894 B |
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 36 36" version="1.1" preserveAspectRatio="xMidYMid meet" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>shield-outline-badged</title>
|
||||
<path d="M30,13.5v2.43c0,11-10,15.61-12,16.43-2-.82-12-5.44-12-16.43V9.14a47.54,47.54,0,0,0,6.18-2.25,48.23,48.23,0,0,0,5.82-3,46.19,46.19,0,0,0,4.51,2.42c0-.1,0-.19,0-.29a7.49,7.49,0,0,1,.23-1.83,41.61,41.61,0,0,1-4.19-2.33L18,1.5l-.54.35a45,45,0,0,1-6.08,3.21A43.79,43.79,0,0,1,4.75,7.4L4,7.59v8.34c0,13.39,13.53,18.4,13.66,18.45l.34.12.34-.12c.14,0,13.66-5.05,13.66-18.45V13.22A7.49,7.49,0,0,1,30,13.5Z" class="clr-i-outline--badged clr-i-outline-path-1--badged"></path><circle cx="30" cy="6" r="5" class="clr-i-outline--badged clr-i-outline-path-2--badged clr-i-badge"></circle>
|
||||
<rect x="0" y="0" width="36" height="36" fill-opacity="0"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1,016 B |
4
src/main/resources/assets/icons/star-svgrepo-com.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.2691 4.41115C11.5006 3.89177 11.6164 3.63208 11.7776 3.55211C11.9176 3.48263 12.082 3.48263 12.222 3.55211C12.3832 3.63208 12.499 3.89177 12.7305 4.41115L14.5745 8.54808C14.643 8.70162 14.6772 8.77839 14.7302 8.83718C14.777 8.8892 14.8343 8.93081 14.8982 8.95929C14.9705 8.99149 15.0541 9.00031 15.2213 9.01795L19.7256 9.49336C20.2911 9.55304 20.5738 9.58288 20.6997 9.71147C20.809 9.82316 20.8598 9.97956 20.837 10.1342C20.8108 10.3122 20.5996 10.5025 20.1772 10.8832L16.8125 13.9154C16.6877 14.0279 16.6252 14.0842 16.5857 14.1527C16.5507 14.2134 16.5288 14.2807 16.5215 14.3503C16.5132 14.429 16.5306 14.5112 16.5655 14.6757L17.5053 19.1064C17.6233 19.6627 17.6823 19.9408 17.5989 20.1002C17.5264 20.2388 17.3934 20.3354 17.2393 20.3615C17.0619 20.3915 16.8156 20.2495 16.323 19.9654L12.3995 17.7024C12.2539 17.6184 12.1811 17.5765 12.1037 17.56C12.0352 17.5455 11.9644 17.5455 11.8959 17.56C11.8185 17.5765 11.7457 17.6184 11.6001 17.7024L7.67662 19.9654C7.18404 20.2495 6.93775 20.3915 6.76034 20.3615C6.60623 20.3354 6.47319 20.2388 6.40075 20.1002C6.31736 19.9408 6.37635 19.6627 6.49434 19.1064L7.4341 14.6757C7.46898 14.5112 7.48642 14.429 7.47814 14.3503C7.47081 14.2807 7.44894 14.2134 7.41394 14.1527C7.37439 14.0842 7.31195 14.0279 7.18708 13.9154L3.82246 10.8832C3.40005 10.5025 3.18884 10.3122 3.16258 10.1342C3.13978 9.97956 3.19059 9.82316 3.29993 9.71147C3.42581 9.58288 3.70856 9.55304 4.27406 9.49336L8.77835 9.01795C8.94553 9.00031 9.02911 8.99149 9.10139 8.95929C9.16534 8.93081 9.2226 8.8892 9.26946 8.83718C9.32241 8.77839 9.35663 8.70162 9.42508 8.54808L11.2691 4.41115Z" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
|
|
@ -1,5 +1,6 @@
|
|||
package creator.loki.util;
|
||||
|
||||
import creator.loki.core.security.CooldownManager;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package creator.loki.util;
|
||||
|
||||
import creator.loki.core.security.RateLimiter;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
|
|
|
|||