diff --git a/TODO.md b/TODO.md index a1cdb6a..63f5bd2 100644 --- a/TODO.md +++ b/TODO.md @@ -197,74 +197,54 @@ src/main/java/creator/loki/ - [x] Добавить `UserStatsRepository` в `BotModule` **3. Удалить импорты AudioManager и мертвые ссылки** -- [ ] Удалить `import creator.loki.audio.AudioManager` из `BotInitializer` -- [ ] Удалить `import creator.loki.audio.AudioManager` из `PrefixCommandRegistrar` -- [ ] Удалить `import creator.loki.audio.AudioManager` из `SlashCommandRegistrar` -- [ ] Удалить параметр `AudioManager audioManager` из конструкторов -- [ ] Удалить поля `private final AudioManager audioManager` -- [ ] Удалить `registry.register(new TtsCommand(...))` из `PrefixCommandRegistrar` -- [ ] Удалить `registry.register(new TtsSlashCommand(...))` из `SlashCommandRegistrar` -- [ ] Исправить `registry.register(new NewLeaderboardCommand(...))` → использовать новый путь -- [ ] Удалить все `registry.register(new *SlashCommand(...))` из `SlashCommandRegistrar` -- [ ] Удалить `import creator.loki.command.slash.*` из `SlashCommandRegistrar` -- [ ] Проверить компиляцию: `gradle build` +- [x] Удалить `import creator.loki.audio.AudioManager` из `BotInitializer` +- [x] Удалить `import creator.loki.audio.AudioManager` из `PrefixCommandRegistrar` +- [x] Удалить `import creator.loki.audio.AudioManager` из `SlashCommandRegistrar` +- [x] Удалить параметр `AudioManager audioManager` из конструкторов +- [x] Удалить поля `private final AudioManager audioManager` +- [x] Удалить `registry.register(new TtsCommand(...))` из `PrefixCommandRegistrar` +- [x] Удалить `registry.register(new TtsSlashCommand(...))` из `SlashCommandRegistrar` +- [x] Исправить `registry.register(new NewLeaderboardCommand(...))` → использовать новый путь +- [x] Удалить все `registry.register(new *SlashCommand(...))` из `SlashCommandRegistrar` +- [x] Удалить `import creator.loki.command.slash.*` из `SlashCommandRegistrar` +- [x] Проверить компиляцию: `gradle build` --- #### Этап 2: feature/level/ (зависит от Этапа 1) **4. Создать feature/level/domain/** -- [ ] Создать `feature/level/` -- [ ] Создать `feature/level/domain/` -- [ ] Создать `feature/level/domain/XpFormula.java`: - ```java - public class XpFormula { - private static final int XP_PER_LEVEL = 200; - - // Исправленная формула: 200 * n * (n+1) / 2 - public int getTotalXPForLevel(int level) { - return XP_PER_LEVEL * level * (level + 1) / 2; - } - - // Обратная формула с использованием XP_PER_LEVEL - public int calculateLevel(int xp) { - double discriminant = 1 + 8.0 * xp / XP_PER_LEVEL; - return (int) Math.floor((-1 + Math.sqrt(discriminant)) / 2.0); - } - } - ``` -- [ ] Вынести `LevelUpResult` из `LevelSystem` → `feature/level/domain/LevelUpResult.java` -- [ ] Вынести `LevelProgress` из `LevelSystem` → `feature/level/domain/LevelProgress.java` -- [ ] Переместить `level/UserStats` → `feature/level/domain/UserStats.java` -- [ ] **НЕ делать immutable сейчас** (Gson требует сеттеры, сделаем после Repository) -- [ ] Добавить `equals()` и `hashCode()` в `UserStats` -- [ ] Переименовать `incrementMessages()` → `recordMessage(long timestamp)` +- [x] Создать `feature/level/` +- [x] Создать `feature/level/domain/` +- [x] Создать `feature/level/domain/XpFormula.java` +- [x] Вынести `LevelUpResult` из `LevelSystem` → `feature/level/domain/LevelUpResult.java` +- [x] Вынести `LevelProgress` из `LevelSystem` → `feature/level/domain/LevelProgress.java` +- [x] Переместить `level/UserStats` → `feature/level/domain/UserStats.java` +- [x] **НЕ делать immutable сейчас** (Gson требует сеттеры, сделаем после Repository) +- [x] Добавить `equals()` и `hashCode()` в `UserStats` +- [x] Переименовать `incrementMessages()` → `recordMessage(long timestamp)` **5. Создать feature/level/service/** -- [ ] Создать `feature/level/service/` -- [ ] Создать `feature/level/service/MessageCooldownService.java`: - - Персистентный кулдаун через `UserStats.lastMessageTime` - - Убрать `ConcurrentHashMap messageTimestamps` -- [ ] Создать `feature/level/service/LeaderboardService.java`: - - Вынести методы `getTopByLevel`, `getTopByMessages`, `getTopByVoice` - - Обобщить через `Comparator` -- [ ] Рефакторить `level/LevelSystem` → `feature/level/service/LevelService.java`: - - Убрать God Class: только бизнес-логика начисления XP - - Использовать `UserStatsRepository` вместо `JsonDataStore` - - Использовать `XpFormula` для вычислений - - Использовать `MessageCooldownService` для кулдаунов - - Убрать методы лидерборда (в `LeaderboardService`) - - Убрать `getAllStats()` (в Repository) +- [x] Создать `feature/level/service/` +- [x] Создать `feature/level/service/MessageCooldownService.java` +- [x] Создать `feature/level/service/LeaderboardService.java` +- [x] Рефакторить `level/LevelSystem` → `feature/level/service/LevelService.java` +- [x] Использовать `UserStatsRepository` вместо `JsonDataStore` +- [x] Использовать `XpFormula` для вычислений +- [x] Использовать `MessageCooldownService` для кулдаунов +- [x] Убрать методы лидерборда (в `LeaderboardService`) +- [x] Обновить все импорты UserStats на новый путь +- [x] Удалить старый `level/UserStats.java` +- [x] Добавить сервисы в `BotModule` **6. Переместить команды и листенеры в feature/level/** -- [ ] Переместить `command/impl/stats/RankCommand` → `feature/level/RankCommand` -- [ ] Переместить `command/impl/stats/StatsCommand` → `feature/level/StatsCommand` -- [ ] Переместить `listener/LevelUpNotifier` → `feature/level/LevelUpNotifier` -- [ ] Переместить `listener/VoiceTracker` → `feature/level/VoiceTracker` -- [ ] Переместить `profile/LevelUtils` → `feature/level/LevelUtils` (если нужен) -- [ ] Удалить пустую папку `level/` -- [ ] Обновить все импорты -- [ ] Обновить `BotInitializer`, `BotModule`, `PrefixCommandRegistrar` +- [x] Переместить `command/impl/stats/RankCommand` → `feature/level/RankCommand` +- [x] Переместить `command/impl/stats/StatsCommand` → `feature/level/StatsCommand` +- [x] Переместить `listener/LevelUpNotifier` → `feature/level/LevelUpNotifier` +- [x] Переместить `listener/VoiceTracker` → `feature/level/VoiceTracker` +- [x] Обновить все импорты +- [x] Обновить `BotInitializer`, `BotModule`, `PrefixCommandRegistrar` +- [x] Обновить команды для использования `LevelService` вместо `LevelSystem` **7. Создать команду сброса уровней (миграция на новую систему)** - [ ] Создать `feature/level/ResetLevelsCommand.java`: diff --git a/src/main/java/creator/loki/command/impl/stats/RankCommand.java b/src/main/java/creator/loki/command/impl/stats/RankCommand.java index c6e1ee6..98f995a 100644 --- a/src/main/java/creator/loki/command/impl/stats/RankCommand.java +++ b/src/main/java/creator/loki/command/impl/stats/RankCommand.java @@ -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; diff --git a/src/main/java/creator/loki/command/impl/stats/leaderboard/LeaderboardFormatter.java b/src/main/java/creator/loki/command/impl/stats/leaderboard/LeaderboardFormatter.java index daaa446..dadf0e7 100644 --- a/src/main/java/creator/loki/command/impl/stats/leaderboard/LeaderboardFormatter.java +++ b/src/main/java/creator/loki/command/impl/stats/leaderboard/LeaderboardFormatter.java @@ -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; diff --git a/src/main/java/creator/loki/command/impl/utility/StopCommand.java b/src/main/java/creator/loki/command/impl/utility/StopCommand.java new file mode 100644 index 0000000..27f205e --- /dev/null +++ b/src/main/java/creator/loki/command/impl/utility/StopCommand.java @@ -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(); + } + }); + } +} diff --git a/src/main/java/creator/loki/core/BotInitializer.java b/src/main/java/creator/loki/core/BotInitializer.java index 5c34b46..f5b8833 100644 --- a/src/main/java/creator/loki/core/BotInitializer.java +++ b/src/main/java/creator/loki/core/BotInitializer.java @@ -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(); diff --git a/src/main/java/creator/loki/core/PrefixCommandRegistrar.java b/src/main/java/creator/loki/core/PrefixCommandRegistrar.java index 77f5749..2f2e42f 100644 --- a/src/main/java/creator/loki/core/PrefixCommandRegistrar.java +++ b/src/main/java/creator/loki/core/PrefixCommandRegistrar.java @@ -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() { diff --git a/src/main/java/creator/loki/core/SlashCommandRegistrar.java b/src/main/java/creator/loki/core/SlashCommandRegistrar.java index 6ace3f8..ba1da0f 100644 --- a/src/main/java/creator/loki/core/SlashCommandRegistrar.java +++ b/src/main/java/creator/loki/core/SlashCommandRegistrar.java @@ -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()); } } diff --git a/src/main/java/creator/loki/di/BotModule.java b/src/main/java/creator/loki/di/BotModule.java index f4a2e48..06deec2 100644 --- a/src/main/java/creator/loki/di/BotModule.java +++ b/src/main/java/creator/loki/di/BotModule.java @@ -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); + } } diff --git a/src/main/java/creator/loki/feature/level/LevelUpNotifier.java b/src/main/java/creator/loki/feature/level/LevelUpNotifier.java new file mode 100644 index 0000000..5f6a15f --- /dev/null +++ b/src/main/java/creator/loki/feature/level/LevelUpNotifier.java @@ -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; + }; + } +} diff --git a/src/main/java/creator/loki/feature/level/RankCommand.java b/src/main/java/creator/loki/feature/level/RankCommand.java new file mode 100644 index 0000000..18b7234 --- /dev/null +++ b/src/main/java/creator/loki/feature/level/RankCommand.java @@ -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(); + } +} diff --git a/src/main/java/creator/loki/feature/level/StatsCommand.java b/src/main/java/creator/loki/feature/level/StatsCommand.java new file mode 100644 index 0000000..6de3de3 --- /dev/null +++ b/src/main/java/creator/loki/feature/level/StatsCommand.java @@ -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(); + } +} diff --git a/src/main/java/creator/loki/feature/level/VoiceTracker.java b/src/main/java/creator/loki/feature/level/VoiceTracker.java new file mode 100644 index 0000000..fa505df --- /dev/null +++ b/src/main/java/creator/loki/feature/level/VoiceTracker.java @@ -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 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 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(); + } + } +} diff --git a/src/main/java/creator/loki/feature/level/domain/LevelProgress.java b/src/main/java/creator/loki/feature/level/domain/LevelProgress.java new file mode 100644 index 0000000..671cba2 --- /dev/null +++ b/src/main/java/creator/loki/feature/level/domain/LevelProgress.java @@ -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); + } +} diff --git a/src/main/java/creator/loki/feature/level/domain/LevelUpResult.java b/src/main/java/creator/loki/feature/level/domain/LevelUpResult.java new file mode 100644 index 0000000..42304dd --- /dev/null +++ b/src/main/java/creator/loki/feature/level/domain/LevelUpResult.java @@ -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); + } +} diff --git a/src/main/java/creator/loki/feature/level/domain/UserStats.java b/src/main/java/creator/loki/feature/level/domain/UserStats.java new file mode 100644 index 0000000..07dc974 --- /dev/null +++ b/src/main/java/creator/loki/feature/level/domain/UserStats.java @@ -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); + } +} diff --git a/src/main/java/creator/loki/feature/level/domain/XpFormula.java b/src/main/java/creator/loki/feature/level/domain/XpFormula.java new file mode 100644 index 0000000..2a6f57b --- /dev/null +++ b/src/main/java/creator/loki/feature/level/domain/XpFormula.java @@ -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)); + } +} diff --git a/src/main/java/creator/loki/feature/level/service/LeaderboardService.java b/src/main/java/creator/loki/feature/level/service/LeaderboardService.java new file mode 100644 index 0000000..d95bedd --- /dev/null +++ b/src/main/java/creator/loki/feature/level/service/LeaderboardService.java @@ -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> getTopByLevel(int limit) { + return getTop(limit, Comparator.comparingInt(stats -> -stats.getLevel())); + } + + /** + * Получить топ пользователей по количеству сообщений + */ + public List> getTopByMessages(int limit) { + return getTop(limit, Comparator.comparingInt(stats -> -stats.getMessageCount())); + } + + /** + * Получить топ пользователей по времени в войсе + */ + public List> getTopByVoice(int limit) { + return getTop(limit, Comparator.comparingLong(stats -> -stats.getVoiceMinutes())); + } + + /** + * Получить позицию пользователя в рейтинге по уровню + */ + public int getUserRankByLevel(String userId) { + List> 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> getTop(int limit, Comparator comparator) { + Map allStats = repository.getAllStats(); + + return allStats.entrySet().stream() + .sorted((e1, e2) -> comparator.compare(e1.getValue(), e2.getValue())) + .limit(limit) + .toList(); + } +} diff --git a/src/main/java/creator/loki/feature/level/service/LevelService.java b/src/main/java/creator/loki/feature/level/service/LevelService.java new file mode 100644 index 0000000..a25db98 --- /dev/null +++ b/src/main/java/creator/loki/feature/level/service/LevelService.java @@ -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); + } +} diff --git a/src/main/java/creator/loki/feature/level/service/MessageCooldownService.java b/src/main/java/creator/loki/feature/level/service/MessageCooldownService.java new file mode 100644 index 0000000..5039ba4 --- /dev/null +++ b/src/main/java/creator/loki/feature/level/service/MessageCooldownService.java @@ -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; + } +} diff --git a/src/main/java/creator/loki/feature/profile/ProfileCommand.java b/src/main/java/creator/loki/feature/profile/ProfileCommand.java new file mode 100644 index 0000000..b682aa4 --- /dev/null +++ b/src/main/java/creator/loki/feature/profile/ProfileCommand.java @@ -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); + } + }); + } +} diff --git a/src/main/java/creator/loki/feature/profile/ProfileData.java b/src/main/java/creator/loki/feature/profile/ProfileData.java new file mode 100644 index 0000000..7957455 --- /dev/null +++ b/src/main/java/creator/loki/feature/profile/ProfileData.java @@ -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; } +} diff --git a/src/main/java/creator/loki/feature/profile/ProfileService.java b/src/main/java/creator/loki/feature/profile/ProfileService.java new file mode 100644 index 0000000..8fc1fe2 --- /dev/null +++ b/src/main/java/creator/loki/feature/profile/ProfileService.java @@ -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"; + } +} diff --git a/src/main/java/creator/loki/feature/profile/RankCardGenerator.java b/src/main/java/creator/loki/feature/profile/RankCardGenerator.java new file mode 100644 index 0000000..83c7118 --- /dev/null +++ b/src/main/java/creator/loki/feature/profile/RankCardGenerator.java @@ -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); + } +} diff --git a/src/main/java/creator/loki/level/LevelSystem.java b/src/main/java/creator/loki/level/LevelSystem.java index 8c133c6..73e95dd 100644 --- a/src/main/java/creator/loki/level/LevelSystem.java +++ b/src/main/java/creator/loki/level/LevelSystem.java @@ -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; } - } } diff --git a/src/main/java/creator/loki/level/UserStats.java b/src/main/java/creator/loki/level/UserStats.java deleted file mode 100644 index 82d030e..0000000 --- a/src/main/java/creator/loki/level/UserStats.java +++ /dev/null @@ -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; - } -} diff --git a/src/main/java/creator/loki/listener/LevelUpNotifier.java b/src/main/java/creator/loki/listener/LevelUpNotifier.java index 41074cb..b64aa87 100644 --- a/src/main/java/creator/loki/listener/LevelUpNotifier.java +++ b/src/main/java/creator/loki/listener/LevelUpNotifier.java @@ -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("🎉 Повышение уровня!") diff --git a/src/main/java/creator/loki/listener/MessageListener.java b/src/main/java/creator/loki/listener/MessageListener.java index 02bb71f..f234710 100644 --- a/src/main/java/creator/loki/listener/MessageListener.java +++ b/src/main/java/creator/loki/listener/MessageListener.java @@ -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( diff --git a/src/main/java/creator/loki/repository/UserStatsRepository.java b/src/main/java/creator/loki/repository/UserStatsRepository.java index 8e4fc77..1ca96bf 100644 --- a/src/main/java/creator/loki/repository/UserStatsRepository.java +++ b/src/main/java/creator/loki/repository/UserStatsRepository.java @@ -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 getAllStats() { + return findAll(); + } } diff --git a/src/main/java/creator/loki/repository/impl/JsonUserStatsRepository.java b/src/main/java/creator/loki/repository/impl/JsonUserStatsRepository.java index 508a106..75dfa66 100644 --- a/src/main/java/creator/loki/repository/impl/JsonUserStatsRepository.java +++ b/src/main/java/creator/loki/repository/impl/JsonUserStatsRepository.java @@ -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; diff --git a/src/main/resources/assets/README.md b/src/main/resources/assets/README.md new file mode 100644 index 0000000..a091cdc --- /dev/null +++ b/src/main/resources/assets/README.md @@ -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/` + +## Примеры изображений + +Можно использовать любые изображения подходящего размера. +Для тестирования можно создать простые цветные заглушки. diff --git a/src/main/resources/assets/backgrounds/fon1.png b/src/main/resources/assets/backgrounds/fon1.png new file mode 100644 index 0000000..ad97307 Binary files /dev/null and b/src/main/resources/assets/backgrounds/fon1.png differ diff --git a/src/main/resources/assets/badges/.gitkeep b/src/main/resources/assets/badges/.gitkeep new file mode 100644 index 0000000..debe500 --- /dev/null +++ b/src/main/resources/assets/badges/.gitkeep @@ -0,0 +1,2 @@ +# Placeholder для бейджей +# Добавьте PNG изображения бейджей (200x200px) diff --git a/src/main/resources/assets/fonts/.gitkeep b/src/main/resources/assets/fonts/.gitkeep new file mode 100644 index 0000000..ae42b3f --- /dev/null +++ b/src/main/resources/assets/fonts/.gitkeep @@ -0,0 +1,2 @@ +# Placeholder для шрифтов +# Скачайте Inter-Bold.ttf и Inter-Regular.ttf с https://fonts.google.com/specimen/Inter diff --git a/src/main/resources/assets/icons/clock-five-thirty-svgrepo-com.svg b/src/main/resources/assets/icons/clock-five-thirty-svgrepo-com.svg new file mode 100644 index 0000000..628b380 --- /dev/null +++ b/src/main/resources/assets/icons/clock-five-thirty-svgrepo-com.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/main/resources/assets/icons/lightning-svgrepo-com.svg b/src/main/resources/assets/icons/lightning-svgrepo-com.svg new file mode 100644 index 0000000..ee161b0 --- /dev/null +++ b/src/main/resources/assets/icons/lightning-svgrepo-com.svg @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/assets/icons/message-square-lines-svgrepo-com.svg b/src/main/resources/assets/icons/message-square-lines-svgrepo-com.svg new file mode 100644 index 0000000..b2f06fc --- /dev/null +++ b/src/main/resources/assets/icons/message-square-lines-svgrepo-com.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/main/resources/assets/icons/shield-alt-1-svgrepo-com.svg b/src/main/resources/assets/icons/shield-alt-1-svgrepo-com.svg new file mode 100644 index 0000000..4017b81 --- /dev/null +++ b/src/main/resources/assets/icons/shield-alt-1-svgrepo-com.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/main/resources/assets/icons/shield-outline-badged-svgrepo-com.svg b/src/main/resources/assets/icons/shield-outline-badged-svgrepo-com.svg new file mode 100644 index 0000000..be6eb97 --- /dev/null +++ b/src/main/resources/assets/icons/shield-outline-badged-svgrepo-com.svg @@ -0,0 +1,6 @@ + + + shield-outline-badged + + + \ No newline at end of file diff --git a/src/main/resources/assets/icons/star-svgrepo-com.svg b/src/main/resources/assets/icons/star-svgrepo-com.svg new file mode 100644 index 0000000..f87660b --- /dev/null +++ b/src/main/resources/assets/icons/star-svgrepo-com.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/test/java/creator/loki/util/CooldownManagerTest.java b/src/test/java/creator/loki/util/CooldownManagerTest.java index 22aa21d..e45e958 100644 --- a/src/test/java/creator/loki/util/CooldownManagerTest.java +++ b/src/test/java/creator/loki/util/CooldownManagerTest.java @@ -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; diff --git a/src/test/java/creator/loki/util/GlobalRateLimiterTest.java b/src/test/java/creator/loki/util/GlobalRateLimiterTest.java index 5c28942..70d5763 100644 --- a/src/test/java/creator/loki/util/GlobalRateLimiterTest.java +++ b/src/test/java/creator/loki/util/GlobalRateLimiterTest.java @@ -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;