v2.3: Major refactoring, Lavalink music system, Spotify support, CI/CD

- Full refactoring: max 200 lines/file, max 3 files/dir, KISS/DRY/SOLID
- Lavalink client 3.4.0 + LavaSrc plugin (Spotify, Apple Music, Deezer)
- 3-bot architecture: main (!), music (q!), radio (r!)
- Filters: bassboost, nightcore, vaporwave, equalizer
- Slash commands for music (/play, /skip, etc.)
- Persistent queue in SQLite, auto-leave timer
- Radio manager with auto-reconnect
- Custom welcome GIF greetings via nekos.life
- JDA 5.6.1, gson 2.14, hikaricp 6.3, all deps updated
- Fixed YouTube playback via ytmsearch (LavaSrc)
- CI/CD: Docker publish + release workflows
- SpotBugs: all real bugs fixed, ignoreFailures=false
- Checkstyle: Google Style (adapted), 0 violations
This commit is contained in:
loki5512344 2026-07-02 23:48:36 +02:00
parent f5398dc779
commit 29a21e4178
301 changed files with 21751 additions and 18099 deletions

View file

@ -2,49 +2,53 @@ package creator.loki.bootstrap;
import com.google.inject.Guice;
import com.google.inject.Injector;
import creator.loki.core.BotCluster;
import creator.loki.core.BotInitializer;
import creator.loki.di.BotModule;
import net.dv8tion.jda.api.JDA;
import creator.loki.util.LoggerDecorator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class App {
private static final Logger logger = LoggerFactory.getLogger(App.class);
private static volatile boolean shutdownRequested = false;
private static final Logger logger = LoggerFactory.getLogger(App.class);
private static volatile boolean shutdownRequested = false;
public static void main(String[] args) {
// Регистрируем shutdown hook ДО создания бота
Thread shutdownHook = new Thread(() -> {
if (!shutdownRequested) {
public static void main(String[] args) {
Thread shutdownHook =
new Thread(
() -> {
if (!shutdownRequested) {
shutdownRequested = true;
logger.info("========================================");
logger.info("Получен сигнал завершения");
logger.info("Выполняется graceful shutdown...");
logger.info("========================================");
}
}, "ShutdownHook");
LoggerDecorator.warn(logger, "Получен сигнал завершения");
LoggerDecorator.warn(logger, "Выполняется graceful shutdown...");
}
},
"ShutdownHook");
Runtime.getRuntime().addShutdownHook(shutdownHook);
Runtime.getRuntime().addShutdownHook(shutdownHook);
try {
logger.info("Запуск Discord бота");
try {
LoggerDecorator.startup(logger, "Запуск Discord ботов");
// Создаём Guice Injector с модулем конфигурации
Injector injector = Guice.createInjector(new BotModule());
Injector injector = Guice.createInjector(new BotModule());
// Получаем BotInitializer с автоматически внедрёнными зависимостями
BotInitializer initializer = injector.getInstance(BotInitializer.class);
BotInitializer initializer = injector.getInstance(BotInitializer.class);
// Запускаем бота
JDA jda = initializer.start();
BotCluster cluster = initializer.start();
// Ждём полной загрузки
jda.awaitReady();
logger.info("Бот полностью готов к работе!");
cluster.getMain().awaitReady();
LoggerDecorator.success(logger, "Main бот полностью готов к работе!");
} catch (Exception e) {
logger.error("Критическая ошибка при запуске бота", e);
System.exit(1);
}
if (cluster.hasMusicBot()) {
LoggerDecorator.success(logger, "Music бот готов к работе!");
}
if (cluster.hasRadioBot()) {
LoggerDecorator.success(logger, "Radio бот готов к работе!");
}
} catch (Exception e) {
LoggerDecorator.error(logger, "Критическая ошибка при запуске ботов", e);
System.exit(1);
}
}
}

View file

@ -1,6 +1,7 @@
package creator.loki.bootstrap;
import com.google.inject.Singleton;
import creator.loki.util.LoggerDecorator;
import net.dv8tion.jda.api.events.session.ReadyEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.slf4j.Logger;
@ -8,11 +9,12 @@ import org.slf4j.LoggerFactory;
@Singleton
public class ReadyListener extends ListenerAdapter {
private static final Logger logger = LoggerFactory.getLogger(ReadyListener.class);
private static final Logger logger = LoggerFactory.getLogger(ReadyListener.class);
@Override
public void onReady(ReadyEvent event) {
logger.info("Бот {} успешно запущен!", event.getJDA().getSelfUser().getName());
logger.info("Подключен к {} серверам", event.getGuildTotalCount());
}
@Override
public void onReady(ReadyEvent event) {
LoggerDecorator.success(
logger, "Бот " + event.getJDA().getSelfUser().getName() + " успешно запущен!");
LoggerDecorator.bot(logger, "Подключен к " + event.getGuildTotalCount() + " серверам");
}
}

View file

@ -1,16 +1,17 @@
package creator.loki.command;
public interface Command {
String getName();
String getDescription();
String getName();
default String getUsage() {
return getName();
}
String getDescription();
default String getCategory() {
return "Общие";
}
default String getUsage() {
return getName();
}
void execute(CommandContext context);
default String getCategory() {
return "Общие";
}
void execute(CommandContext context);
}

View file

@ -1,12 +1,10 @@
package creator.loki.command;
/**
* Имена категорий для {@link Command#getCategory()} (справка, селектор help).
*/
/** Имена категорий для {@link Command#getCategory()} (справка, селектор help). */
public final class CommandCategories {
/** Команды с GIF (nekos.life): hug, slap, poke и т.п. */
public static final String GIF_INTERACTIVE = "GIF-интерактив";
/** Команды с GIF (nekos.life): hug, slap, poke и т.п. */
public static final String GIF_INTERACTIVE = "GIF-интерактив";
private CommandCategories() {}
private CommandCategories() {}
}

View file

@ -1,44 +1,45 @@
package creator.loki.command;
import java.util.List;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import java.util.List;
public class CommandContext {
private final Message message;
private final List<String> args;
private final MessageChannel channel;
private final Member author;
private final Guild guild;
private final Message message;
private final List<String> args;
private final MessageChannel channel;
private final Member author;
private final Guild guild;
public CommandContext(Message message, List<String> args, MessageChannel channel, Member author, Guild guild) {
this.message = message;
this.args = args;
this.channel = channel;
this.author = author;
this.guild = guild;
}
public CommandContext(
Message message, List<String> args, MessageChannel channel, Member author, Guild guild) {
this.message = message;
this.args = args;
this.channel = channel;
this.author = author;
this.guild = guild;
}
public Message getMessage() {
return message;
}
public Message getMessage() {
return message;
}
public List<String> getArgs() {
return args;
}
public List<String> getArgs() {
return args;
}
public MessageChannel getChannel() {
return channel;
}
public MessageChannel getChannel() {
return channel;
}
public Member getAuthor() {
return author;
}
public Member getAuthor() {
return author;
}
public Guild getGuild() {
return guild;
}
public Guild getGuild() {
return guild;
}
}

View file

@ -1,43 +1,74 @@
package creator.loki.config;
public class BotConfig {
private final String token;
private final String prefix;
private final String activity;
private final int healthHttpPort;
private final String superAdminId;
private final String dbPath;
private final String mainToken;
private final String musicToken;
private final String radioToken;
private final String prefix;
private final String activity;
private final int healthHttpPort;
private final String superAdminId;
private final String dbPath;
private final LavalinkConfig lavalinkConfig;
public BotConfig(String token, String prefix, String activity, int healthHttpPort, String superAdminId, String dbPath) {
this.token = token;
this.prefix = prefix;
this.activity = activity;
this.healthHttpPort = healthHttpPort;
this.superAdminId = superAdminId;
this.dbPath = dbPath;
}
public BotConfig(
String mainToken,
String musicToken,
String radioToken,
String prefix,
String activity,
int healthHttpPort,
String superAdminId,
String dbPath,
LavalinkConfig lavalinkConfig) {
this.mainToken = mainToken;
this.musicToken = musicToken;
this.radioToken = radioToken;
this.prefix = prefix;
this.activity = activity;
this.healthHttpPort = healthHttpPort;
this.superAdminId = superAdminId;
this.dbPath = dbPath;
this.lavalinkConfig = lavalinkConfig;
}
public String getToken() {
return token;
}
public String getMainToken() {
return mainToken;
}
public String getPrefix() {
return prefix;
}
public String getToken() {
return mainToken;
}
public String getActivity() {
return activity;
}
public String getMusicToken() {
return musicToken;
}
public int getHealthHttpPort() {
return healthHttpPort;
}
public String getRadioToken() {
return radioToken;
}
public String getSuperAdminId() {
return superAdminId;
}
public String getPrefix() {
return prefix;
}
public String getDbPath() {
return dbPath;
}
public String getActivity() {
return activity;
}
public int getHealthHttpPort() {
return healthHttpPort;
}
public String getSuperAdminId() {
return superAdminId;
}
public String getDbPath() {
return dbPath;
}
public LavalinkConfig getLavalinkConfig() {
return lavalinkConfig;
}
}

View file

@ -1,67 +1,89 @@
package creator.loki.config;
import creator.loki.util.LoggerDecorator;
import io.github.cdimascio.dotenv.Dotenv;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class ConfigLoader {
private static final Logger logger = LoggerFactory.getLogger(ConfigLoader.class);
private static final Logger logger = LoggerFactory.getLogger(ConfigLoader.class);
private ConfigLoader() {
private ConfigLoader() {}
public static LavalinkConfig getLavalinkConfig() {
Dotenv dotenv = Dotenv.configure().ignoreIfMissing().load();
return new LavalinkConfig(
getEnv(dotenv, "LAVALINK_HOST", "127.0.0.1"),
Integer.parseInt(getEnv(dotenv, "LAVALINK_PORT", "2333")),
getEnv(dotenv, "LAVALINK_PASSWORD", "youshallnotpass"),
Boolean.parseBoolean(getEnv(dotenv, "LAVALINK_ENABLED", "true")));
}
private static String getEnv(Dotenv dotenv, String key, String defaultValue) {
String value = dotenv.get(key);
return (value != null && !value.isBlank()) ? value : defaultValue;
}
private static int parsePort(String raw, String key) {
if (raw == null || raw.isBlank()) return 0;
try {
int port = Integer.parseInt(raw.trim());
if (port < 0 || port > 65535) {
throw new IllegalArgumentException("порт вне диапазона 0..65535");
}
return port;
} catch (NumberFormatException e) {
throw new IllegalStateException(
"Некорректный " + key + ": \"" + raw + "\". Ожидается число 0..65535.", e);
}
}
public static BotConfig load() {
Dotenv dotenv = Dotenv.configure().ignoreIfMissing().load();
String mainToken = dotenv.get("DISCORD_BOT_TOKEN");
if (mainToken == null || mainToken.isBlank()) {
throw new IllegalStateException(
"Токен бота не найден! Установите переменную окружения DISCORD_BOT_TOKEN или создайте .env файл");
}
private static int parsePort(String raw, String key) {
if (raw == null || raw.isBlank()) return 0;
try {
int port = Integer.parseInt(raw.trim());
if (port < 0 || port > 65535) {
throw new IllegalArgumentException("порт вне диапазона 0..65535");
}
return port;
} catch (NumberFormatException e) {
throw new IllegalStateException(
"Некорректный " + key + ": \"" + raw + "\". Ожидается число 0..65535.",
e
);
}
String musicToken = dotenv.get("DISCORD_BOT_TOKEN_MUSIC");
String radioToken = dotenv.get("DISCORD_BOT_TOKEN_RADIO");
String prefix = dotenv.get("BOT_PREFIX");
if (prefix == null || prefix.isBlank()) {
prefix = "!";
}
public static BotConfig load() {
Dotenv dotenv = Dotenv.configure()
.ignoreIfMissing()
.load();
String token = dotenv.get("DISCORD_BOT_TOKEN");
if (token == null || token.isBlank()) {
throw new IllegalStateException(
"Токен бота не найден! Установите переменную окружения DISCORD_BOT_TOKEN или создайте .env файл"
);
}
String prefix = dotenv.get("BOT_PREFIX");
if (prefix == null || prefix.isBlank()) {
prefix = "!";
}
String activity = dotenv.get("BOT_ACTIVITY");
if (activity == null || activity.isBlank()) {
activity = "Используй !help";
}
int healthHttpPort = parsePort(dotenv.get("HEALTH_HTTP_PORT"), "HEALTH_HTTP_PORT");
String superAdminId = dotenv.get("SUPER_ADMIN_ID");
if (superAdminId == null || superAdminId.isBlank()) {
superAdminId = "1414975197269987471";
logger.warn("SUPER_ADMIN_ID не задан, используется значение по умолчанию");
}
String dbPath = dotenv.get("DB_PATH");
if (dbPath == null || dbPath.isBlank()) {
dbPath = "data/bot.db";
}
logger.info("Конфигурация загружена успешно");
return new BotConfig(token, prefix, activity, healthHttpPort, superAdminId, dbPath);
String activity = dotenv.get("BOT_ACTIVITY");
if (activity == null || activity.isBlank()) {
activity = "Используй !help";
}
int healthHttpPort = parsePort(dotenv.get("HEALTH_HTTP_PORT"), "HEALTH_HTTP_PORT");
String superAdminId = dotenv.get("SUPER_ADMIN_ID");
if (superAdminId == null || superAdminId.isBlank()) {
superAdminId = "1414975197269987471";
LoggerDecorator.warn(logger, "SUPER_ADMIN_ID не задан, используется значение по умолчанию");
}
String dbPath = dotenv.get("DB_PATH");
if (dbPath == null || dbPath.isBlank()) {
dbPath = "data/bot.db";
}
LavalinkConfig lavalinkConfig = getLavalinkConfig();
LoggerDecorator.success(logger, "Конфигурация загружена успешно");
return new BotConfig(
mainToken,
musicToken,
radioToken,
prefix,
activity,
healthHttpPort,
superAdminId,
dbPath,
lavalinkConfig);
}
}

View file

@ -0,0 +1,39 @@
package creator.loki.config;
public class LavalinkConfig {
private final String host;
private final int port;
private final String password;
private final boolean enabled;
public LavalinkConfig(String host, int port, String password, boolean enabled) {
this.host = host;
this.port = port;
this.password = password;
this.enabled = enabled;
}
public String getHost() {
return host;
}
public int getPort() {
return port;
}
public String getPassword() {
return password;
}
public boolean isEnabled() {
return enabled;
}
public String getUri() {
return "http://" + host + ":" + port;
}
public String getWsUri() {
return "ws://" + host + ":" + port;
}
}

View file

@ -0,0 +1,35 @@
package creator.loki.core;
import net.dv8tion.jda.api.JDA;
public class BotCluster {
private final JDA main;
private final JDA music;
private final JDA radio;
public BotCluster(JDA main, JDA music, JDA radio) {
this.main = main;
this.music = music;
this.radio = radio;
}
public JDA getMain() {
return main;
}
public JDA getMusic() {
return music;
}
public JDA getRadio() {
return radio;
}
public boolean hasMusicBot() {
return music != null;
}
public boolean hasRadioBot() {
return radio != null;
}
}

View file

@ -4,82 +4,174 @@ import com.google.inject.Inject;
import creator.loki.config.BotConfig;
import creator.loki.core.registry.PrefixCommandRegistrar;
import creator.loki.feature.counter.MemberCounterListener;
import creator.loki.feature.moderation.MuteScheduler;
import creator.loki.feature.metrics.HealthHttpServer;
import creator.loki.feature.moderation.MuteScheduler;
import creator.loki.feature.music.core.LavalinkManager;
import creator.loki.feature.music.listener.MusicBotListener;
import creator.loki.feature.music.repository.QueueRepository;
import creator.loki.feature.radio.RadioBotListener;
import creator.loki.feature.radio.RadioManager;
import creator.loki.util.LoggerDecorator;
import dev.arbjerg.lavalink.libraries.jda.JDAVoiceUpdateListener;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.JDABuilder;
import net.dv8tion.jda.api.OnlineStatus;
import net.dv8tion.jda.api.entities.Activity;
import net.dv8tion.jda.api.requests.GatewayIntent;
import net.dv8tion.jda.api.utils.cache.CacheFlag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class BotInitializer {
private static final Logger logger = LoggerFactory.getLogger(BotInitializer.class);
private static final Logger logger = LoggerFactory.getLogger(BotInitializer.class);
private final BotConfig config;
private final PrefixCommandRegistrar registrar;
private final ListenerAggregator listenerAggregator;
private final MuteScheduler muteScheduler;
private final MemberCounterListener memberCounterListener;
private final HealthHttpServer healthHttpServer;
private final ShutdownManager shutdownManager;
private final BotConfig config;
private final PrefixCommandRegistrar registrar;
private final ListenerAggregator listenerAggregator;
private final MuteScheduler muteScheduler;
private final MemberCounterListener memberCounterListener;
private final HealthHttpServer healthHttpServer;
private final ShutdownManager shutdownManager;
private final LavalinkManager lavalinkManager;
private final RadioManager radioManager;
private final QueueRepository queueRepository;
@Inject
public BotInitializer(
BotConfig config,
PrefixCommandRegistrar registrar,
ListenerAggregator listenerAggregator,
MuteScheduler muteScheduler,
MemberCounterListener memberCounterListener,
HealthHttpServer healthHttpServer,
ShutdownManager shutdownManager
) {
this.config = config;
this.registrar = registrar;
this.listenerAggregator = listenerAggregator;
this.muteScheduler = muteScheduler;
this.memberCounterListener = memberCounterListener;
this.healthHttpServer = healthHttpServer;
this.shutdownManager = shutdownManager;
@Inject
public BotInitializer(
BotConfig config,
PrefixCommandRegistrar registrar,
ListenerAggregator listenerAggregator,
MuteScheduler muteScheduler,
MemberCounterListener memberCounterListener,
HealthHttpServer healthHttpServer,
ShutdownManager shutdownManager,
LavalinkManager lavalinkManager,
RadioManager radioManager,
QueueRepository queueRepository) {
this.config = config;
this.registrar = registrar;
this.listenerAggregator = listenerAggregator;
this.muteScheduler = muteScheduler;
this.memberCounterListener = memberCounterListener;
this.healthHttpServer = healthHttpServer;
this.shutdownManager = shutdownManager;
this.lavalinkManager = lavalinkManager;
this.radioManager = radioManager;
this.queueRepository = queueRepository;
}
public BotCluster start() {
LoggerDecorator.startup(logger, "Инициализация main бота...");
registrar.registerAll();
JDA mainJda = createMainJDA();
try {
mainJda.awaitReady();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Main бот прерван во время ожидания готовности", e);
}
mainJda.updateCommands().queue();
muteScheduler.restorePending(mainJda);
for (var guild : mainJda.getGuilds()) {
memberCounterListener.startScheduler(guild);
}
healthHttpServer.start();
JDA musicJda = null;
if (config.getMusicToken() != null && !config.getMusicToken().isBlank()) {
LoggerDecorator.startup(logger, "Инициализация music бота...");
musicJda = createMusicJDA();
try {
musicJda.awaitReady();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Music бот прерван во время ожидания готовности", e);
}
}
public JDA start() {
logger.info("Инициализация бота...");
registrar.registerAll();
JDA jda = createJDA();
try {
jda.awaitReady();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Бот прерван во время ожидания готовности", e);
}
jda.updateCommands().queue();
muteScheduler.restorePending(jda);
for (var guild : jda.getGuilds()) {
memberCounterListener.startScheduler(guild);
}
healthHttpServer.start();
registerShutdownHook(jda);
return jda;
JDA radioJda = null;
if (config.getRadioToken() != null && !config.getRadioToken().isBlank()) {
LoggerDecorator.startup(logger, "Инициализация radio бота...");
radioJda = createRadioJDA();
try {
radioJda.awaitReady();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Radio бот прерван во время ожидания готовности", e);
}
radioManager.setRadioJda(radioJda);
}
private JDA createJDA() {
return JDABuilder.createDefault(config.getToken())
.setStatus(OnlineStatus.ONLINE)
.setActivity(Activity.playing(config.getActivity()))
.enableIntents(
GatewayIntent.GUILD_MESSAGES,
GatewayIntent.MESSAGE_CONTENT,
GatewayIntent.GUILD_MEMBERS,
GatewayIntent.GUILD_VOICE_STATES
)
.addEventListeners(listenerAggregator.getAll())
.build();
BotCluster cluster = new BotCluster(mainJda, musicJda, radioJda);
registerShutdownHook(cluster);
return cluster;
}
private JDA createMainJDA() {
var builder =
JDABuilder.createDefault(config.getMainToken())
.setStatus(OnlineStatus.ONLINE)
.setActivity(Activity.playing(config.getActivity()))
.enableIntents(
GatewayIntent.GUILD_MESSAGES,
GatewayIntent.MESSAGE_CONTENT,
GatewayIntent.GUILD_MEMBERS,
GatewayIntent.GUILD_VOICE_STATES)
.enableCache(CacheFlag.VOICE_STATE)
.addEventListeners(listenerAggregator.getAll());
if (lavalinkManager.getClient() != null) {
builder.setVoiceDispatchInterceptor(new JDAVoiceUpdateListener(lavalinkManager.getClient()));
}
private void registerShutdownHook(JDA jda) {
Runtime.getRuntime().addShutdownHook(new Thread(
() -> shutdownManager.shutdown(jda), "ShutdownHook"));
return builder.build();
}
private JDA createMusicJDA() {
LavalinkManager musicLavalink =
new LavalinkManager(config, queueRepository, config.getMusicToken());
var builder =
JDABuilder.createDefault(config.getMusicToken())
.setStatus(OnlineStatus.ONLINE)
.setActivity(Activity.playing("🎵 q!play"))
.enableIntents(
GatewayIntent.GUILD_MESSAGES,
GatewayIntent.MESSAGE_CONTENT,
GatewayIntent.GUILD_VOICE_STATES)
.enableCache(CacheFlag.VOICE_STATE);
if (musicLavalink.getClient() != null) {
builder.setVoiceDispatchInterceptor(new JDAVoiceUpdateListener(musicLavalink.getClient()));
}
builder.addEventListeners(
new MusicBotListener(
musicLavalink,
queueRepository,
new creator.loki.feature.music.core.PlayerManager(musicLavalink)));
return builder.build();
}
private JDA createRadioJDA() {
var builder =
JDABuilder.createDefault(config.getRadioToken())
.setStatus(OnlineStatus.ONLINE)
.setActivity(Activity.playing("📻 r!play"))
.enableIntents(
GatewayIntent.GUILD_MESSAGES,
GatewayIntent.MESSAGE_CONTENT,
GatewayIntent.GUILD_VOICE_STATES)
.enableCache(CacheFlag.VOICE_STATE);
builder.addEventListeners(new RadioBotListener(radioManager));
return builder.build();
}
private void registerShutdownHook(BotCluster cluster) {
Runtime.getRuntime()
.addShutdownHook(new Thread(() -> shutdownManager.shutdown(cluster), "ShutdownHook"));
}
}

View file

@ -1,130 +1,146 @@
package creator.loki.core;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.config.BotConfig;
import creator.loki.core.registry.CommandRegistry;
import creator.loki.embed.EmbedTemplates;
import creator.loki.core.exception.CommandExceptionHandler;
import creator.loki.feature.metrics.MetricsCollector;
import creator.loki.core.security.RateLimiter;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class EventDispatcher {
private static final Logger logger = LoggerFactory.getLogger(EventDispatcher.class);
private final BotConfig config;
private final CommandRegistry registry;
private final ExecutorService executorService;
private final RateLimiter rateLimiter;
private final MetricsCollector metrics;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.config.BotConfig;
import creator.loki.core.exception.CommandExceptionHandler;
import creator.loki.core.registry.CommandRegistry;
import creator.loki.core.security.RateLimiter;
import creator.loki.embed.EmbedTemplates;
import creator.loki.feature.metrics.MetricsCollector;
import creator.loki.util.LoggerDecorator;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public EventDispatcher(BotConfig config, CommandRegistry registry, RateLimiter rateLimiter, MetricsCollector metrics) {
this.config = config;
this.registry = registry;
this.executorService = Executors.newVirtualThreadPerTaskExecutor();
this.rateLimiter = rateLimiter;
this.metrics = metrics;
public class EventDispatcher {
private static final Logger logger = LoggerFactory.getLogger(EventDispatcher.class);
private final BotConfig config;
private final CommandRegistry registry;
private final ExecutorService executorService;
private final RateLimiter rateLimiter;
private final MetricsCollector metrics;
public EventDispatcher(
BotConfig config,
CommandRegistry registry,
RateLimiter rateLimiter,
MetricsCollector metrics) {
this.config = config;
this.registry = registry;
this.executorService = Executors.newVirtualThreadPerTaskExecutor();
this.rateLimiter = rateLimiter;
this.metrics = metrics;
}
public void handleMessage(MessageReceivedEvent event) {
// Игнорируем ботов
if (event.getAuthor().isBot()) {
return;
}
public void handleMessage(MessageReceivedEvent event) {
// Игнорируем ботов
if (event.getAuthor().isBot()) {
return;
}
String content = event.getMessage().getContentRaw();
if (!content.startsWith(config.getPrefix())) {
return;
}
String content = event.getMessage().getContentRaw();
if (!content.startsWith(config.getPrefix())) {
return;
}
// Парсим команду и аргументы
String[] parts = content.substring(config.getPrefix().length()).trim().split("\\s+");
if (parts.length == 0) {
return;
}
// Парсим команду и аргументы
String[] parts = content.substring(config.getPrefix().length()).trim().split("\\s+");
if (parts.length == 0) {
return;
}
String commandName = parts[0].toLowerCase();
List<String> args =
parts.length > 1 ? Arrays.asList(Arrays.copyOfRange(parts, 1, parts.length)) : List.of();
String commandName = parts[0].toLowerCase();
List<String> args = parts.length > 1
? Arrays.asList(Arrays.copyOfRange(parts, 1, parts.length))
: List.of();
Command command = registry.getCommand(commandName);
if (command == null) {
event
.getChannel()
.sendMessageEmbeds(
EmbedTemplates.error(
"Команда не найдена",
"Команда `"
+ commandName
+ "` не существует. Используй `"
+ config.getPrefix()
+ "help`"))
.queue();
return;
}
Command command = registry.getCommand(commandName);
if (command == null) {
event.getChannel().sendMessageEmbeds(
EmbedTemplates.error(
"Команда не найдена",
"Команда `" + commandName + "` не существует. Используй `" + config.getPrefix() + "help`"
)
).queue();
return;
}
// Проверяем глобальный rate limit
String userId = event.getAuthor().getId();
String guildId = event.getGuild().getId();
// Проверяем глобальный rate limit
String userId = event.getAuthor().getId();
String guildId = event.getGuild() != null ? event.getGuild().getId() : "DM";
if (!rateLimiter.allow(userId, guildId)) {
long waitTime = rateLimiter.getUserWaitTime(userId);
event
.getChannel()
.sendMessageEmbeds(
EmbedTemplates.error(
"⏱️ Слишком быстро!",
"Ты отправляешь команды слишком часто. Подожди " + waitTime + " секунд."))
.queue();
LoggerDecorator.warn(
logger, "Rate limit для пользователя " + userId + " в гильдии " + guildId);
return;
}
if (!rateLimiter.allow(userId, guildId)) {
long waitTime = rateLimiter.getUserWaitTime(userId);
event.getChannel().sendMessageEmbeds(
EmbedTemplates.error(
"⏱️ Слишком быстро!",
"Ты отправляешь команды слишком часто. Подожди " + waitTime + " секунд."
)
).queue();
logger.warn("Rate limit для пользователя {} в гильдии {}", userId, guildId);
return;
}
// Выполняем команду асинхронно
executorService.submit(() -> {
long startTime = System.currentTimeMillis();
try {
CommandContext context = new CommandContext(
// Выполняем команду асинхронно
executorService.submit(
() -> {
long startTime = System.currentTimeMillis();
try {
CommandContext context =
new CommandContext(
event.getMessage(),
args,
event.getChannel(),
event.getMember(),
event.getGuild()
);
command.execute(context);
event.getGuild());
command.execute(context);
// Записываем метрики
long executionTime = System.currentTimeMillis() - startTime;
metrics.recordCommand(commandName, executionTime);
// Записываем метрики
long executionTime = System.currentTimeMillis() - startTime;
metrics.recordCommand(commandName, executionTime);
logger.info("Выполнена команда: {} от {} за {} мс",
commandName, event.getAuthor().getName(), executionTime);
} catch (Exception e) {
// Записываем ошибку в метрики
metrics.recordError(commandName);
LoggerDecorator.command(
logger,
commandName,
event.getAuthor().getName(),
"Выполнена за " + executionTime + " мс");
} catch (Exception e) {
// Записываем ошибку в метрики
metrics.recordError(commandName);
// Используем централизованный обработчик ошибок
CommandExceptionHandler.handle(e, event.getChannel(), commandName);
}
// Используем централизованный обработчик ошибок
CommandExceptionHandler.handle(e, event.getChannel(), commandName);
}
});
}
}
public void shutdown() {
logger.info("Завершение работы EventDispatcher...");
executorService.shutdown();
try {
if (!executorService.awaitTermination(10, TimeUnit.SECONDS)) {
logger.warn("ExecutorService не завершился за 10 секунд, принудительная остановка");
executorService.shutdownNow();
}
} catch (InterruptedException e) {
logger.error("Прервано ожидание завершения ExecutorService", e);
executorService.shutdownNow();
Thread.currentThread().interrupt();
}
logger.info("EventDispatcher завершён");
public void shutdown() {
LoggerDecorator.bot(logger, "Завершение работы EventDispatcher...");
executorService.shutdown();
try {
if (!executorService.awaitTermination(10, TimeUnit.SECONDS)) {
LoggerDecorator.warn(
logger, "ExecutorService не завершился за 10 секунд, принудительная остановка");
executorService.shutdownNow();
}
} catch (InterruptedException e) {
LoggerDecorator.error(logger, "Прервано ожидание завершения ExecutorService", e);
executorService.shutdownNow();
Thread.currentThread().interrupt();
}
LoggerDecorator.success(logger, "EventDispatcher завершён");
}
}

View file

@ -2,15 +2,16 @@ package creator.loki.core;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import creator.loki.feature.level.MessageListener;
import creator.loki.feature.moderation.ModerationLogger;
import creator.loki.bootstrap.ReadyListener;
import creator.loki.feature.help.HelpMenuHandler;
import creator.loki.feature.counter.MemberCounterListener;
import creator.loki.feature.economy.FishMessageListener;
import creator.loki.feature.help.HelpMenuHandler;
import creator.loki.feature.leaderboard.LeaderboardButtonListener;
import creator.loki.feature.level.LevelUpNotifier;
import creator.loki.feature.level.MessageListener;
import creator.loki.feature.level.ResetLevelsButtonHandler;
import creator.loki.feature.level.VoiceTracker;
import creator.loki.feature.moderation.log.ModerationLogger;
import creator.loki.feature.music.MusicSlashCommands;
import creator.loki.feature.voice.VoiceButtonHandler;
import creator.loki.feature.voice.VoiceChannelListener;
import creator.loki.feature.voice.VoiceModalHandler;
@ -19,36 +20,48 @@ import creator.loki.feature.welcome.WelcomeListener;
@Singleton
public class ListenerAggregator {
private final Object[] listeners;
private final Object[] listeners;
@Inject
public ListenerAggregator(
ReadyListener readyListener,
MessageListener messageListener,
VoiceTracker voiceTracker,
VoiceChannelListener voiceChannelListener,
MemberCounterListener memberCounterListener,
VoiceButtonHandler voiceButtonHandler,
VoiceModalHandler voiceModalHandler,
VoiceSyncListener voiceSyncListener,
WelcomeListener welcomeListener,
ModerationLogger moderationLogger,
HelpMenuHandler helpMenuHandler,
ResetLevelsButtonHandler resetLevelsButtonHandler,
LeaderboardButtonListener leaderboardButtonListener,
MessageCache messageCache
) {
this.listeners = new Object[]{
readyListener, messageListener, voiceTracker,
voiceChannelListener, memberCounterListener,
voiceButtonHandler, voiceModalHandler, voiceSyncListener,
welcomeListener, moderationLogger, helpMenuHandler,
resetLevelsButtonHandler, leaderboardButtonListener,
messageCache
@Inject
public ListenerAggregator(
ReadyListener readyListener,
MessageListener messageListener,
VoiceTracker voiceTracker,
VoiceChannelListener voiceChannelListener,
MemberCounterListener memberCounterListener,
VoiceButtonHandler voiceButtonHandler,
VoiceModalHandler voiceModalHandler,
VoiceSyncListener voiceSyncListener,
WelcomeListener welcomeListener,
ModerationLogger moderationLogger,
HelpMenuHandler helpMenuHandler,
ResetLevelsButtonHandler resetLevelsButtonHandler,
LeaderboardButtonListener leaderboardButtonListener,
MessageCache messageCache,
MusicSlashCommands musicSlashCommands,
FishMessageListener fishMessageListener) {
this.listeners =
new Object[] {
readyListener,
messageListener,
voiceTracker,
voiceChannelListener,
memberCounterListener,
voiceButtonHandler,
voiceModalHandler,
voiceSyncListener,
welcomeListener,
moderationLogger,
helpMenuHandler,
resetLevelsButtonHandler,
leaderboardButtonListener,
messageCache,
musicSlashCommands,
fishMessageListener
};
}
}
public Object[] getAll() {
return listeners;
}
public Object[] getAll() {
return listeners;
}
}

View file

@ -1,32 +1,30 @@
package creator.loki.core;
import java.util.concurrent.TimeUnit;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import java.util.concurrent.TimeUnit;
public class MessageCache extends ListenerAdapter {
private final Cache<String, String> cache;
private final Cache<String, String> cache;
public MessageCache() {
this.cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(1, TimeUnit.HOURS)
.build();
}
public MessageCache() {
this.cache =
Caffeine.newBuilder().maximumSize(10_000).expireAfterWrite(1, TimeUnit.HOURS).build();
}
@Override
public void onMessageReceived(MessageReceivedEvent event) {
if (event.getAuthor().isBot()) return;
String content = event.getMessage().getContentRaw();
if (!content.isBlank()) {
cache.put(event.getMessageId(), content);
}
@Override
public void onMessageReceived(MessageReceivedEvent event) {
if (event.getAuthor().isBot()) return;
String content = event.getMessage().getContentRaw();
if (!content.isBlank()) {
cache.put(event.getMessageId(), content);
}
}
public String getContent(String messageId) {
return cache.getIfPresent(messageId);
}
public String getContent(String messageId) {
return cache.getIfPresent(messageId);
}
}

View file

@ -1,85 +1,118 @@
package creator.loki.core;
import java.time.Duration;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import creator.loki.core.security.CooldownManager;
import creator.loki.core.security.RateLimiter;
import creator.loki.database.Database;
import creator.loki.feature.counter.MemberCounterListener;
import creator.loki.feature.level.VoiceTracker;
import creator.loki.feature.moderation.MuteScheduler;
import creator.loki.feature.metrics.HealthHttpServer;
import creator.loki.core.security.CooldownManager;
import creator.loki.core.security.RateLimiter;
import creator.loki.feature.moderation.MuteScheduler;
import creator.loki.feature.radio.RadioManager;
import creator.loki.util.LoggerDecorator;
import net.dv8tion.jda.api.JDA;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.Duration;
@Singleton
public class ShutdownManager {
private static final Logger logger = LoggerFactory.getLogger(ShutdownManager.class);
private static final Logger logger = LoggerFactory.getLogger(ShutdownManager.class);
private final HealthHttpServer healthHttpServer;
private final CooldownManager cooldownManager;
private final RateLimiter rateLimiter;
private final VoiceTracker voiceTracker;
private final MemberCounterListener memberCounterListener;
private final MuteScheduler muteScheduler;
private final EventDispatcher dispatcher;
private final Database database;
private final HealthHttpServer healthHttpServer;
private final CooldownManager cooldownManager;
private final RateLimiter rateLimiter;
private final VoiceTracker voiceTracker;
private final MemberCounterListener memberCounterListener;
private final MuteScheduler muteScheduler;
private final EventDispatcher dispatcher;
private final Database database;
private final RadioManager radioManager;
@Inject
public ShutdownManager(
HealthHttpServer healthHttpServer,
CooldownManager cooldownManager,
RateLimiter rateLimiter,
VoiceTracker voiceTracker,
MemberCounterListener memberCounterListener,
MuteScheduler muteScheduler,
EventDispatcher dispatcher,
Database database
) {
this.healthHttpServer = healthHttpServer;
this.cooldownManager = cooldownManager;
this.rateLimiter = rateLimiter;
this.voiceTracker = voiceTracker;
this.memberCounterListener = memberCounterListener;
this.muteScheduler = muteScheduler;
this.dispatcher = dispatcher;
this.database = database;
@Inject
public ShutdownManager(
HealthHttpServer healthHttpServer,
CooldownManager cooldownManager,
RateLimiter rateLimiter,
VoiceTracker voiceTracker,
MemberCounterListener memberCounterListener,
MuteScheduler muteScheduler,
EventDispatcher dispatcher,
Database database,
RadioManager radioManager) {
this.healthHttpServer = healthHttpServer;
this.cooldownManager = cooldownManager;
this.rateLimiter = rateLimiter;
this.voiceTracker = voiceTracker;
this.memberCounterListener = memberCounterListener;
this.muteScheduler = muteScheduler;
this.dispatcher = dispatcher;
this.database = database;
this.radioManager = radioManager;
}
public void shutdown(JDA jda) {
shutdown(new BotCluster(jda, null, null));
}
public void shutdown() {
shutdown(new BotCluster(null, null, null));
}
public void shutdown(BotCluster cluster) {
logger.info("========================================");
logger.info("Получен сигнал завершения работы ботов...");
logger.info("========================================");
try {
// Stop all radio playback
if (cluster.hasRadioBot()) {
radioManager.getRadioJda().getGuilds().forEach(g -> radioManager.stopRadio(g.getIdLong()));
}
healthHttpServer.stop();
LoggerDecorator.bot(logger, "Остановка менеджеров...");
cooldownManager.shutdown();
rateLimiter.shutdown();
voiceTracker.shutdown();
memberCounterListener.shutdown();
muteScheduler.shutdown();
dispatcher.shutdown();
LoggerDecorator.database(logger, "Закрытие базы данных...");
database.close();
shutdownJDA(cluster.getMain(), "Main");
if (cluster.hasMusicBot()) {
shutdownJDA(cluster.getMusic(), "Music");
}
if (cluster.hasRadioBot()) {
shutdownJDA(cluster.getRadio(), "Radio");
}
} catch (Exception e) {
LoggerDecorator.error(logger, "Ошибка при завершении работы ботов", e);
}
public void shutdown(JDA jda) {
logger.info("========================================");
logger.info("Получен сигнал завершения работы бота...");
logger.info("========================================");
LoggerDecorator.success(logger, "Боты успешно завершены!");
}
try {
healthHttpServer.stop();
logger.info("Остановка менеджеров...");
cooldownManager.shutdown();
rateLimiter.shutdown();
voiceTracker.shutdown();
memberCounterListener.shutdown();
muteScheduler.shutdown();
dispatcher.shutdown();
logger.info("Закрытие базы данных...");
database.close();
logger.info("Отключение JDA...");
jda.shutdown();
if (!jda.awaitShutdown(Duration.ofSeconds(10))) {
logger.warn("JDA не завершился за 10 секунд, принудительная остановка");
jda.shutdownNow();
}
} catch (Exception e) {
logger.error("Ошибка при завершении работы бота", e);
}
logger.info("========================================");
logger.info("Бот успешно завершён!");
logger.info("========================================");
private void shutdownJDA(JDA jda, String name) {
LoggerDecorator.bot(logger, "Отключение " + name + " JDA...");
jda.shutdown();
try {
if (!jda.awaitShutdown(Duration.ofSeconds(10))) {
LoggerDecorator.warn(
logger, name + " JDA не завершился за 10 секунд, принудительная остановка");
jda.shutdownNow();
}
} catch (InterruptedException e) {
LoggerDecorator.warn(logger, name + " JDA shutdown прерван");
jda.shutdownNow();
Thread.currentThread().interrupt();
}
}
}

View file

@ -1,23 +1,21 @@
package creator.loki.core.exception;
/**
* Исключение для ошибок выполнения команд
*/
/** Исключение для ошибок выполнения команд */
public class CommandException extends Exception {
private static final long serialVersionUID = 1L;
private final String userMessage;
private static final long serialVersionUID = 1L;
private final String userMessage;
public CommandException(String userMessage) {
super(userMessage);
this.userMessage = userMessage;
}
public CommandException(String userMessage) {
super(userMessage);
this.userMessage = userMessage;
}
public CommandException(String userMessage, Throwable cause) {
super(userMessage, cause);
this.userMessage = userMessage;
}
public CommandException(String userMessage, Throwable cause) {
super(userMessage, cause);
this.userMessage = userMessage;
}
public String getUserMessage() {
return userMessage;
}
public String getUserMessage() {
return userMessage;
}
}

View file

@ -1,33 +1,32 @@
package creator.loki.core.exception;
import java.awt.Color;
import creator.loki.embed.EmbedFactory;
import creator.loki.util.LoggerDecorator;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.Color;
/**
* Обработчик исключений для команд
*/
/** Обработчик исключений для команд */
public class CommandExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(CommandExceptionHandler.class);
private static final Logger logger = LoggerFactory.getLogger(CommandExceptionHandler.class);
/**
* Обрабатывает исключение и отправляет сообщение пользователю
*/
public static void handle(Throwable throwable, MessageChannel channel, String commandName) {
logger.error("Ошибка при выполнении команды: {}", commandName, throwable);
/** Обрабатывает исключение и отправляет сообщение пользователю */
public static void handle(Throwable throwable, MessageChannel channel, String commandName) {
LoggerDecorator.error(logger, "Ошибка при выполнении команды: " + commandName, throwable);
String userMessage;
if (throwable instanceof CommandException ce) {
userMessage = ce.getUserMessage();
} else {
userMessage = "Не удалось выполнить команду. Попробуйте позже или обратитесь к администратору.";
}
String userMessage;
if (throwable instanceof CommandException ce) {
userMessage = ce.getUserMessage();
} else {
userMessage =
"Не удалось выполнить команду. Попробуйте позже или обратитесь к администратору.";
}
MessageEmbed errorEmbed = EmbedFactory.create()
MessageEmbed errorEmbed =
EmbedFactory.create()
.withTitle("❌ Ошибка")
.withDescription(userMessage)
.withColor(Color.RED)
@ -35,19 +34,21 @@ public class CommandExceptionHandler {
.withFooter("Если ошибка повторяется, обратитесь к администратору")
.build();
channel.sendMessageEmbeds(errorEmbed).queue(
success -> { },
error -> logger.error("Не удалось отправить сообщение об ошибке", error)
);
}
channel
.sendMessageEmbeds(errorEmbed)
.queue(
success -> {},
error ->
LoggerDecorator.error(logger, "Не удалось отправить сообщение об ошибке", error));
}
/**
* Обрабатывает исключение с кастомным сообщением
*/
public static void handle(Throwable throwable, MessageChannel channel, String commandName, String customMessage) {
logger.error("Ошибка при выполнении команды: {}", commandName, throwable);
/** Обрабатывает исключение с кастомным сообщением */
public static void handle(
Throwable throwable, MessageChannel channel, String commandName, String customMessage) {
LoggerDecorator.error(logger, "Ошибка при выполнении команды: " + commandName, throwable);
MessageEmbed errorEmbed = EmbedFactory.create()
MessageEmbed errorEmbed =
EmbedFactory.create()
.withTitle("❌ Ошибка")
.withDescription(customMessage)
.withColor(Color.RED)
@ -55,9 +56,11 @@ public class CommandExceptionHandler {
.withFooter("Если ошибка повторяется, обратитесь к администратору")
.build();
channel.sendMessageEmbeds(errorEmbed).queue(
success -> { },
error -> logger.error("Не удалось отправить сообщение об ошибке", error)
);
}
channel
.sendMessageEmbeds(errorEmbed)
.queue(
success -> {},
error ->
LoggerDecorator.error(logger, "Не удалось отправить сообщение об ошибке", error));
}
}

View file

@ -2,5 +2,5 @@ package creator.loki.core.registry;
@FunctionalInterface
public interface CommandRegistrar {
void register(CommandRegistry registry);
void register(CommandRegistry registry);
}

View file

@ -1,38 +1,45 @@
package creator.loki.core.registry;
import creator.loki.command.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import creator.loki.command.Command;
import creator.loki.util.LoggerDecorator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class CommandRegistry {
private static final Logger logger = LoggerFactory.getLogger(CommandRegistry.class);
private final Map<String, Command> commands = new HashMap<>();
private static final Logger logger = LoggerFactory.getLogger(CommandRegistry.class);
private final Map<String, Command> commands = new HashMap<>();
public void register(Command command) {
String key = command.getName().toLowerCase();
Command previous = commands.put(key, command);
if (previous != null) {
logger.warn("Имя команды «{}» уже занято: {} заменена на {}",
key, previous.getClass().getSimpleName(), command.getClass().getSimpleName());
} else {
logger.debug("Зарегистрирована команда: {}", command.getName());
}
public void register(Command command) {
String key = command.getName().toLowerCase();
Command previous = commands.put(key, command);
if (previous != null) {
LoggerDecorator.warn(
logger,
"Имя команды «"
+ key
+ "» уже занято: "
+ previous.getClass().getSimpleName()
+ " заменена на "
+ command.getClass().getSimpleName());
} else {
logger.debug("Зарегистрирована команда: {}", command.getName());
}
}
public Command getCommand(String name) {
return commands.get(name.toLowerCase());
}
public Command getCommand(String name) {
return commands.get(name.toLowerCase());
}
public Collection<Command> getAllCommands() {
return Collections.unmodifiableCollection(commands.values());
}
public Collection<Command> getAllCommands() {
return Collections.unmodifiableCollection(commands.values());
}
public int getCommandCount() {
return commands.size();
}
public int getCommandCount() {
return commands.size();
}
}

View file

@ -1,29 +1,31 @@
package creator.loki.core.registry;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Set;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import creator.loki.util.LoggerDecorator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Singleton
public class PrefixCommandRegistrar {
private static final Logger logger = LoggerFactory.getLogger(PrefixCommandRegistrar.class);
private static final Logger logger = LoggerFactory.getLogger(PrefixCommandRegistrar.class);
private final CommandRegistry registry;
private final Set<CommandRegistrar> registrars;
private final CommandRegistry registry;
private final Set<CommandRegistrar> registrars;
@Inject
public PrefixCommandRegistrar(CommandRegistry registry, Set<CommandRegistrar> registrars) {
this.registry = registry;
this.registrars = registrars;
}
public void registerAll() {
for (CommandRegistrar registrar : registrars) {
registrar.register(registry);
}
logger.info("Зарегистрировано префиксных команд: {}", registry.getCommandCount());
@Inject
public PrefixCommandRegistrar(CommandRegistry registry, Set<CommandRegistrar> registrars) {
this.registry = registry;
this.registrars = registrars;
}
public void registerAll() {
for (CommandRegistrar registrar : registrars) {
registrar.register(registry);
}
LoggerDecorator.success(
logger, "Зарегистрировано префиксных команд: " + registry.getCommandCount());
}
}

View file

@ -7,77 +7,69 @@ import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class CooldownManager {
private final ConcurrentHashMap<String, Instant> cooldowns = new ConcurrentHashMap<>();
private final ScheduledExecutorService cleanupExecutor = Executors.newSingleThreadScheduledExecutor();
private final ConcurrentHashMap<String, Instant> cooldowns = new ConcurrentHashMap<>();
private final ScheduledExecutorService cleanupExecutor =
Executors.newSingleThreadScheduledExecutor();
@SuppressWarnings("this-escape")
public CooldownManager() {
cleanupExecutor.scheduleAtFixedRate(() -> Thread.startVirtualThread(this::cleanup), 5, 5, TimeUnit.MINUTES);
@SuppressWarnings("this-escape")
public CooldownManager() {
cleanupExecutor.scheduleAtFixedRate(
() -> Thread.startVirtualThread(this::cleanup), 5, 5, TimeUnit.MINUTES);
}
/** Проверяет, находится ли пользователь на cooldown для команды */
public boolean isOnCooldown(String userId, String commandName) {
String key = userId + ":" + commandName;
Instant cooldownEnd = cooldowns.get(key);
if (cooldownEnd == null) {
return false;
}
/**
* Проверяет, находится ли пользователь на cooldown для команды
*/
public boolean isOnCooldown(String userId, String commandName) {
String key = userId + ":" + commandName;
Instant cooldownEnd = cooldowns.get(key);
if (cooldownEnd == null) {
return false;
}
if (Instant.now().isAfter(cooldownEnd)) {
cooldowns.remove(key);
return false;
}
return true;
if (Instant.now().isAfter(cooldownEnd)) {
cooldowns.remove(key);
return false;
}
/**
* Возвращает оставшееся время cooldown в секундах
*/
public long getRemainingSeconds(String userId, String commandName) {
String key = userId + ":" + commandName;
Instant cooldownEnd = cooldowns.get(key);
return true;
}
if (cooldownEnd == null) {
return 0;
}
/** Возвращает оставшееся время cooldown в секундах */
public long getRemainingSeconds(String userId, String commandName) {
String key = userId + ":" + commandName;
Instant cooldownEnd = cooldowns.get(key);
long remaining = cooldownEnd.getEpochSecond() - Instant.now().getEpochSecond();
return Math.max(0, remaining);
if (cooldownEnd == null) {
return 0;
}
/**
* Применяет cooldown для пользователя на команду
*/
public void applyCooldown(String userId, String commandName, int seconds) {
String key = userId + ":" + commandName;
Instant cooldownEnd = Instant.now().plusSeconds(seconds);
cooldowns.put(key, cooldownEnd);
}
long remaining = cooldownEnd.getEpochSecond() - Instant.now().getEpochSecond();
return Math.max(0, remaining);
}
/**
* Очищает истёкшие cooldown'ы
*/
public void cleanup() {
Instant now = Instant.now();
cooldowns.entrySet().removeIf(entry -> now.isAfter(entry.getValue()));
}
/** Применяет cooldown для пользователя на команду */
public void applyCooldown(String userId, String commandName, int seconds) {
String key = userId + ":" + commandName;
Instant cooldownEnd = Instant.now().plusSeconds(seconds);
cooldowns.put(key, cooldownEnd);
}
/**
* Завершает работу менеджера
*/
public void shutdown() {
cleanupExecutor.shutdown();
try {
if (!cleanupExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
cleanupExecutor.shutdownNow();
}
} catch (InterruptedException e) {
cleanupExecutor.shutdownNow();
Thread.currentThread().interrupt();
}
/** Очищает истёкшие cooldown'ы */
public void cleanup() {
Instant now = Instant.now();
cooldowns.entrySet().removeIf(entry -> now.isAfter(entry.getValue()));
}
/** Завершает работу менеджера */
public void shutdown() {
cleanupExecutor.shutdown();
try {
if (!cleanupExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
cleanupExecutor.shutdownNow();
}
} catch (InterruptedException e) {
cleanupExecutor.shutdownNow();
Thread.currentThread().interrupt();
}
}
}

View file

@ -1,163 +1,150 @@
package creator.loki.core.security;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.Instant;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* Rate limiter для защиты от спама
* Использует алгоритм Token Bucket
*/
import creator.loki.util.LoggerDecorator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** Rate limiter для защиты от спама Использует алгоритм Token Bucket */
public class RateLimiter {
private static final Logger logger = LoggerFactory.getLogger(RateLimiter.class);
private static final Logger logger = LoggerFactory.getLogger(RateLimiter.class);
// Лимиты по умолчанию
private static final int DEFAULT_USER_CAPACITY = 10; // Максимум токенов на пользователя
private static final int DEFAULT_USER_REFILL_RATE = 2; // Токенов в секунду
private static final int DEFAULT_GUILD_CAPACITY = 50; // Максимум токенов на гильдию
private static final int DEFAULT_GUILD_REFILL_RATE = 10; // Токенов в секунду
// Лимиты по умолчанию
private static final int DEFAULT_USER_CAPACITY = 10; // Максимум токенов на пользователя
private static final int DEFAULT_USER_REFILL_RATE = 2; // Токенов в секунду
private static final int DEFAULT_GUILD_CAPACITY = 50; // Максимум токенов на гильдию
private static final int DEFAULT_GUILD_REFILL_RATE = 10; // Токенов в секунду
private final ConcurrentHashMap<String, TokenBucket> userBuckets = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, TokenBucket> guildBuckets = new ConcurrentHashMap<>();
private final ScheduledExecutorService cleanupExecutor = Executors.newSingleThreadScheduledExecutor();
private final ConcurrentHashMap<String, TokenBucket> userBuckets = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, TokenBucket> guildBuckets = new ConcurrentHashMap<>();
private final ScheduledExecutorService cleanupExecutor =
Executors.newSingleThreadScheduledExecutor();
public RateLimiter() {
// Очистка старых bucket'ов каждые 5 минут
cleanupExecutor.scheduleAtFixedRate(() -> Thread.startVirtualThread(this::cleanup), 5, 5, TimeUnit.MINUTES);
logger.info("RateLimiter инициализирован");
public RateLimiter() {
// Очистка старых bucket'ов каждые 5 минут
cleanupExecutor.scheduleAtFixedRate(
() -> Thread.startVirtualThread(this::cleanup), 5, 5, TimeUnit.MINUTES);
LoggerDecorator.bot(logger, "RateLimiter инициализирован");
}
/** Проверяет, может ли пользователь выполнить действие */
public boolean allowUser(String userId) {
TokenBucket bucket =
userBuckets.computeIfAbsent(
userId, k -> new TokenBucket(DEFAULT_USER_CAPACITY, DEFAULT_USER_REFILL_RATE));
return bucket.tryConsume();
}
/** Проверяет, может ли гильдия выполнить действие */
public boolean allowGuild(String guildId) {
TokenBucket bucket =
guildBuckets.computeIfAbsent(
guildId, k -> new TokenBucket(DEFAULT_GUILD_CAPACITY, DEFAULT_GUILD_REFILL_RATE));
return bucket.tryConsume();
}
/** Проверяет оба лимита: пользователя и гильдии */
public boolean allow(String userId, String guildId) {
return allowUser(userId) && allowGuild(guildId);
}
/** Возвращает оставшееся время до восстановления для пользователя (в секундах) */
public long getUserWaitTime(String userId) {
TokenBucket bucket = userBuckets.get(userId);
if (bucket == null) {
return 0;
}
return bucket.getWaitTime();
}
/** Очищает неактивные bucket'ы */
private void cleanup() {
long now = System.currentTimeMillis();
long threshold = now - TimeUnit.MINUTES.toMillis(10);
int usersBefore = userBuckets.size();
int guildsBefore = guildBuckets.size();
userBuckets.entrySet().removeIf(entry -> entry.getValue().getLastAccessTime() < threshold);
guildBuckets.entrySet().removeIf(entry -> entry.getValue().getLastAccessTime() < threshold);
int removedUsers = usersBefore - userBuckets.size();
int removedGuilds = guildsBefore - guildBuckets.size();
if (removedUsers > 0 || removedGuilds > 0) {
logger.debug(
"Очистка rate limiter: удалено {} user / {} guild buckets, осталось {} / {}",
removedUsers,
removedGuilds,
userBuckets.size(),
guildBuckets.size());
}
}
/** Завершает работу rate limiter */
public void shutdown() {
LoggerDecorator.bot(logger, "Завершение работы RateLimiter...");
cleanupExecutor.shutdown();
try {
if (!cleanupExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
cleanupExecutor.shutdownNow();
}
} catch (InterruptedException e) {
cleanupExecutor.shutdownNow();
Thread.currentThread().interrupt();
}
LoggerDecorator.success(logger, "RateLimiter завершён");
}
/** Token Bucket для rate limiting */
private static class TokenBucket {
private final int capacity;
private final double refillRate; // токенов в секунду
private double tokens;
private long lastRefillTime;
private long lastAccessTime;
public TokenBucket(int capacity, double refillRate) {
this.capacity = capacity;
this.refillRate = refillRate;
this.tokens = capacity;
this.lastRefillTime = System.currentTimeMillis();
this.lastAccessTime = System.currentTimeMillis();
}
/**
* Проверяет, может ли пользователь выполнить действие
*/
public boolean allowUser(String userId) {
TokenBucket bucket = userBuckets.computeIfAbsent(
userId,
k -> new TokenBucket(DEFAULT_USER_CAPACITY, DEFAULT_USER_REFILL_RATE)
);
return bucket.tryConsume();
public synchronized boolean tryConsume() {
refill();
lastAccessTime = System.currentTimeMillis();
if (tokens >= 1.0) {
tokens -= 1.0;
return true;
}
return false;
}
/**
* Проверяет, может ли гильдия выполнить действие
*/
public boolean allowGuild(String guildId) {
TokenBucket bucket = guildBuckets.computeIfAbsent(
guildId,
k -> new TokenBucket(DEFAULT_GUILD_CAPACITY, DEFAULT_GUILD_REFILL_RATE)
);
return bucket.tryConsume();
private void refill() {
long now = System.currentTimeMillis();
long timePassed = now - lastRefillTime;
double tokensToAdd = (timePassed / 1000.0) * refillRate;
tokens = Math.min(capacity, tokens + tokensToAdd);
lastRefillTime = now;
}
/**
* Проверяет оба лимита: пользователя и гильдии
*/
public boolean allow(String userId, String guildId) {
return allowUser(userId) && allowGuild(guildId);
public synchronized long getWaitTime() {
refill();
if (tokens >= 1.0) {
return 0;
}
double tokensNeeded = 1.0 - tokens;
return (long) Math.ceil(tokensNeeded / refillRate);
}
/**
* Возвращает оставшееся время до восстановления для пользователя (в секундах)
*/
public long getUserWaitTime(String userId) {
TokenBucket bucket = userBuckets.get(userId);
if (bucket == null) {
return 0;
}
return bucket.getWaitTime();
}
/**
* Очищает неактивные bucket'ы
*/
private void cleanup() {
long now = System.currentTimeMillis();
long threshold = now - TimeUnit.MINUTES.toMillis(10);
int usersBefore = userBuckets.size();
int guildsBefore = guildBuckets.size();
userBuckets.entrySet().removeIf(entry -> entry.getValue().getLastAccessTime() < threshold);
guildBuckets.entrySet().removeIf(entry -> entry.getValue().getLastAccessTime() < threshold);
int removedUsers = usersBefore - userBuckets.size();
int removedGuilds = guildsBefore - guildBuckets.size();
if (removedUsers > 0 || removedGuilds > 0) {
logger.debug("Очистка rate limiter: удалено {} user / {} guild buckets, осталось {} / {}",
removedUsers, removedGuilds, userBuckets.size(), guildBuckets.size());
}
}
/**
* Завершает работу rate limiter
*/
public void shutdown() {
logger.info("Завершение работы RateLimiter...");
cleanupExecutor.shutdown();
try {
if (!cleanupExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
cleanupExecutor.shutdownNow();
}
} catch (InterruptedException e) {
cleanupExecutor.shutdownNow();
Thread.currentThread().interrupt();
}
logger.info("RateLimiter завершён");
}
/**
* Token Bucket для rate limiting
*/
private static class TokenBucket {
private final int capacity;
private final double refillRate; // токенов в секунду
private double tokens;
private long lastRefillTime;
private long lastAccessTime;
public TokenBucket(int capacity, double refillRate) {
this.capacity = capacity;
this.refillRate = refillRate;
this.tokens = capacity;
this.lastRefillTime = System.currentTimeMillis();
this.lastAccessTime = System.currentTimeMillis();
}
public synchronized boolean tryConsume() {
refill();
lastAccessTime = System.currentTimeMillis();
if (tokens >= 1.0) {
tokens -= 1.0;
return true;
}
return false;
}
private void refill() {
long now = System.currentTimeMillis();
long timePassed = now - lastRefillTime;
double tokensToAdd = (timePassed / 1000.0) * refillRate;
tokens = Math.min(capacity, tokens + tokensToAdd);
lastRefillTime = now;
}
public synchronized long getWaitTime() {
refill();
if (tokens >= 1.0) {
return 0;
}
double tokensNeeded = 1.0 - tokens;
return (long) Math.ceil(tokensNeeded / refillRate);
}
public long getLastAccessTime() {
return lastAccessTime;
}
public long getLastAccessTime() {
return lastAccessTime;
}
}
}

View file

@ -1,90 +1,108 @@
package creator.loki.database;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Statement;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import creator.loki.util.LoggerDecorator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Database {
private static final Logger logger = LoggerFactory.getLogger(Database.class);
private final String dbPath;
private HikariDataSource dataSource;
private static final Logger logger = LoggerFactory.getLogger(Database.class);
private final String dbPath;
private HikariDataSource dataSource;
public Database(String dbPath) {
this.dbPath = dbPath;
public Database(String dbPath) {
this.dbPath = dbPath;
}
public void connect() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:sqlite:" + dbPath);
config.setMaximumPoolSize(5);
config.setConnectionTimeout(10000);
config.setIdleTimeout(0);
config.setMaxLifetime(600000);
config.setPoolName("LokiDB");
dataSource = new HikariDataSource(config);
LoggerDecorator.database(
logger,
"Подключение к базе данных: "
+ dbPath
+ " (pool size: "
+ config.getMaximumPoolSize()
+ ")");
try (Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement()) {
stmt.execute("PRAGMA foreign_keys = ON");
} catch (SQLException e) {
LoggerDecorator.error(logger, "Ошибка включения foreign keys", e);
}
}
public void connect() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:sqlite:" + dbPath);
config.setMaximumPoolSize(5);
config.setConnectionTimeout(10000);
config.setIdleTimeout(300000);
config.setMaxLifetime(600000);
config.setPoolName("LokiDB");
dataSource = new HikariDataSource(config);
logger.info("Подключение к базе данных: {} (pool size: {})", dbPath, config.getMaximumPoolSize());
try (Connection conn = dataSource.getConnection(); Statement stmt = conn.createStatement()) {
stmt.execute("PRAGMA foreign_keys = ON");
} catch (SQLException e) {
logger.error("Ошибка включения foreign keys", e);
}
public Connection getConnection() {
try {
return dataSource.getConnection();
} catch (SQLException e) {
LoggerDecorator.error(logger, "Ошибка получения соединения из пула", e);
throw new RuntimeException(e);
}
}
public Connection getConnection() {
try {
return dataSource.getConnection();
} catch (SQLException e) {
logger.error("Ошибка получения соединения из пула", e);
throw new RuntimeException(e);
}
public void execute(String sql) {
try (Connection conn = getConnection();
Statement stmt = conn.createStatement()) {
stmt.execute(sql);
} catch (SQLException e) {
LoggerDecorator.error(logger, "Ошибка выполнения SQL: " + sql, e);
throw new RuntimeException(e);
}
}
public void execute(String sql) {
try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) {
stmt.execute(sql);
} catch (SQLException e) {
logger.error("Ошибка выполнения SQL: {}", sql, e);
throw new RuntimeException(e);
}
}
public PreparedStatement prepare(String sql) {
try {
Connection conn = getConnection();
PreparedStatement stmt = conn.prepareStatement(sql);
return (PreparedStatement) Proxy.newProxyInstance(
PreparedStatement.class.getClassLoader(),
new Class<?>[]{PreparedStatement.class},
(proxy, method, args) -> {
if (method.getName().equals("close")) {
try {
return method.invoke(stmt, args);
} finally {
conn.close();
}
}
public PreparedStatement prepare(String sql) {
Connection conn = null;
try {
conn = getConnection();
PreparedStatement stmt = conn.prepareStatement(sql);
Connection finalConn = conn;
return (PreparedStatement)
Proxy.newProxyInstance(
PreparedStatement.class.getClassLoader(),
new Class<?>[] {PreparedStatement.class},
(proxy, method, args) -> {
if (method.getName().equals("close")) {
try {
return method.invoke(stmt, args);
} finally {
finalConn.close();
}
}
);
} catch (SQLException e) {
logger.error("Ошибка подготовки SQL: {}", sql, e);
throw new RuntimeException(e);
return method.invoke(stmt, args);
});
} catch (SQLException e) {
if (conn != null) {
try {
conn.close();
} catch (SQLException ignored) {
/* ignore */
}
}
LoggerDecorator.error(logger, "Ошибка подготовки SQL: " + sql, e);
throw new RuntimeException(e);
}
}
public void close() {
if (dataSource != null && !dataSource.isClosed()) {
dataSource.close();
logger.info("Пул соединений закрыт");
}
public void close() {
if (dataSource != null && !dataSource.isClosed()) {
dataSource.close();
LoggerDecorator.database(logger, "Пул соединений закрыт");
}
}
}

View file

@ -1,190 +1,196 @@
package creator.loki.database;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.FileReader;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Map;
/**
* Миграция данных из JSON в SQLite
*/
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import creator.loki.util.LoggerDecorator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** Миграция данных из JSON в SQLite */
public class DatabaseMigration {
private static final Logger logger = LoggerFactory.getLogger(DatabaseMigration.class);
private final Database database;
private final Gson gson = new Gson();
private static final Logger logger = LoggerFactory.getLogger(DatabaseMigration.class);
private final Database database;
private final Gson gson = new Gson();
public DatabaseMigration(Database database) {
this.database = database;
public DatabaseMigration(Database database) {
this.database = database;
}
/** Мигрировать данные из JSON */
public void migrateFromJson(String jsonPath) {
LoggerDecorator.database(logger, "Начало миграции данных из JSON: " + jsonPath);
if (!new java.io.File(jsonPath).exists()) {
logger.info("No legacy data file found at {}, skipping migration", jsonPath);
return;
}
/**
* Мигрировать данные из JSON
*/
public void migrateFromJson(String jsonPath) {
logger.info("Начало миграции данных из JSON: {}", jsonPath);
try (InputStreamReader reader =
new InputStreamReader(new FileInputStream(jsonPath), StandardCharsets.UTF_8)) {
Type type = new TypeToken<Map<String, Object>>() {}.getType();
Map<String, Object> data = gson.fromJson(reader, type);
try (FileReader reader = new FileReader(jsonPath)) {
Type type = new TypeToken<Map<String, Object>>(){}.getType();
Map<String, Object> data = gson.fromJson(reader, type);
if (data == null) {
LoggerDecorator.warn(logger, "JSON файл пустой или не существует");
return;
}
if (data == null) {
logger.warn("JSON файл пустой или не существует");
return;
}
if (isMigrated()) {
LoggerDecorator.database(logger, "Данные уже мигрированы, пропускаем");
return;
}
if (isMigrated()) {
logger.info("Данные уже мигрированы, пропускаем");
return;
}
migrateUserStats(data);
migrateReputation(data);
migrateShipPairs(data);
migrateCustomRoles(data);
markMigrated();
migrateUserStats(data);
migrateReputation(data);
migrateShipPairs(data);
migrateCustomRoles(data);
markMigrated();
LoggerDecorator.success(logger, "Миграция завершена успешно");
logger.info("Миграция завершена успешно");
} catch (Exception e) {
logger.error("Ошибка миграции данных", e);
}
} catch (Exception e) {
LoggerDecorator.error(logger, "Ошибка миграции данных", e);
}
}
private boolean isMigrated() {
try (PreparedStatement stmt = database.prepare("SELECT value FROM config WHERE key = 'migrated'")) {
ResultSet rs = stmt.executeQuery();
return rs.next() && "true".equals(rs.getString("value"));
} catch (SQLException e) {
return false;
}
private boolean isMigrated() {
try (PreparedStatement stmt =
database.prepare("SELECT value FROM config WHERE key = 'migrated'")) {
ResultSet rs = stmt.executeQuery();
return rs.next() && "true".equals(rs.getString("value"));
} catch (SQLException e) {
return false;
}
}
private void markMigrated() {
try (PreparedStatement stmt = database.prepare(
"INSERT OR REPLACE INTO config (key, value) VALUES ('migrated', 'true')"
)) {
stmt.executeUpdate();
} catch (SQLException e) {
logger.error("Ошибка сохранения статуса миграции", e);
}
private void markMigrated() {
try (PreparedStatement stmt =
database.prepare(
"INSERT OR REPLACE INTO config (key, value) VALUES ('migrated', 'true')")) {
stmt.executeUpdate();
} catch (SQLException e) {
LoggerDecorator.error(logger, "Ошибка сохранения статуса миграции", e);
}
}
@SuppressWarnings("unchecked")
private void migrateUserStats(Map<String, Object> data) {
Map<String, Object> userStats = (Map<String, Object>) data.get("userStats");
if (userStats == null) return;
@SuppressWarnings("unchecked")
private void migrateUserStats(Map<String, Object> data) {
Map<String, Object> userStats = (Map<String, Object>) data.get("userStats");
if (userStats == null) return;
logger.info("Миграция userStats: {} записей", userStats.size());
LoggerDecorator.database(logger, "Миграция userStats: " + userStats.size() + " записей");
String sql = """
String sql =
"""
INSERT OR REPLACE INTO user_stats
(user_id, level, total_xp, message_count, voice_minutes, last_message_time, last_voice_join)
VALUES (?, ?, ?, ?, ?, ?, ?)""";
try (PreparedStatement stmt = database.prepare(sql)) {
for (Map.Entry<String, Object> entry : userStats.entrySet()) {
Map<String, Object> stats = (Map<String, Object>) entry.getValue();
try (PreparedStatement stmt = database.prepare(sql)) {
for (Map.Entry<String, Object> entry : userStats.entrySet()) {
Map<String, Object> stats = (Map<String, Object>) entry.getValue();
stmt.setString(1, (String) stats.get("userId"));
stmt.setInt(2, ((Number) stats.get("level")).intValue());
stmt.setInt(3, ((Number) stats.get("totalXP")).intValue());
stmt.setInt(4, ((Number) stats.get("messageCount")).intValue());
stmt.setLong(5, ((Number) stats.get("voiceMinutes")).longValue());
stmt.setLong(6, ((Number) stats.get("lastMessageTime")).longValue());
stmt.setLong(7, ((Number) stats.get("lastVoiceJoin")).longValue());
stmt.setString(1, (String) stats.get("userId"));
stmt.setInt(2, ((Number) stats.get("level")).intValue());
stmt.setInt(3, ((Number) stats.get("totalXP")).intValue());
stmt.setInt(4, ((Number) stats.get("messageCount")).intValue());
stmt.setLong(5, ((Number) stats.get("voiceMinutes")).longValue());
stmt.setLong(6, ((Number) stats.get("lastMessageTime")).longValue());
stmt.setLong(7, ((Number) stats.get("lastVoiceJoin")).longValue());
stmt.executeUpdate();
}
logger.info("userStats мигрированы");
} catch (SQLException e) {
logger.error("Ошибка миграции userStats", e);
}
stmt.executeUpdate();
}
LoggerDecorator.success(logger, "userStats мигрированы");
} catch (SQLException e) {
LoggerDecorator.error(logger, "Ошибка миграции userStats", e);
}
}
@SuppressWarnings("unchecked")
private void migrateReputation(Map<String, Object> data) {
Map<String, Object> reputation = (Map<String, Object>) data.get("reputation");
if (reputation == null) return;
@SuppressWarnings("unchecked")
private void migrateReputation(Map<String, Object> data) {
Map<String, Object> reputation = (Map<String, Object>) data.get("reputation");
if (reputation == null) return;
logger.info("Миграция reputation: {} записей", reputation.size());
LoggerDecorator.database(logger, "Миграция reputation: " + reputation.size() + " записей");
try (PreparedStatement stmt = database.prepare(
"INSERT OR REPLACE INTO reputation (user_id, reputation) VALUES (?, ?)"
)) {
for (Map.Entry<String, Object> entry : reputation.entrySet()) {
stmt.setString(1, entry.getKey());
stmt.setDouble(2, ((Number) entry.getValue()).doubleValue());
stmt.executeUpdate();
}
logger.info("reputation мигрирована");
} catch (SQLException e) {
logger.error("Ошибка миграции reputation", e);
}
try (PreparedStatement stmt =
database.prepare("INSERT OR REPLACE INTO reputation (user_id, reputation) VALUES (?, ?)")) {
for (Map.Entry<String, Object> entry : reputation.entrySet()) {
stmt.setString(1, entry.getKey());
stmt.setDouble(2, ((Number) entry.getValue()).doubleValue());
stmt.executeUpdate();
}
LoggerDecorator.success(logger, "reputation мигрирована");
} catch (SQLException e) {
LoggerDecorator.error(logger, "Ошибка миграции reputation", e);
}
}
@SuppressWarnings("unchecked")
private void migrateShipPairs(Map<String, Object> data) {
Map<String, Object> shipPairs = (Map<String, Object>) data.get("shipPairs");
if (shipPairs == null) return;
@SuppressWarnings("unchecked")
private void migrateShipPairs(Map<String, Object> data) {
Map<String, Object> shipPairs = (Map<String, Object>) data.get("shipPairs");
if (shipPairs == null) return;
logger.info("Миграция shipPairs: {} записей", shipPairs.size());
LoggerDecorator.database(logger, "Миграция shipPairs: " + shipPairs.size() + " записей");
try (PreparedStatement stmt = database.prepare(
"INSERT OR REPLACE INTO ship_pairs (user1_id, user2_id, compatibility) VALUES (?, ?, ?)"
)) {
for (Map.Entry<String, Object> entry : shipPairs.entrySet()) {
String[] users = entry.getKey().split("-");
if (users.length == 2) {
stmt.setString(1, users[0]);
stmt.setString(2, users[1]);
stmt.setDouble(3, ((Number) entry.getValue()).doubleValue());
stmt.executeUpdate();
}
}
logger.info("shipPairs мигрированы");
} catch (SQLException e) {
logger.error("Ошибка миграции shipPairs", e);
try (PreparedStatement stmt =
database.prepare(
"INSERT OR REPLACE INTO ship_pairs (user1_id, user2_id, compatibility) VALUES (?, ?, ?)")) {
for (Map.Entry<String, Object> entry : shipPairs.entrySet()) {
String[] users = entry.getKey().split("-");
if (users.length == 2) {
stmt.setString(1, users[0]);
stmt.setString(2, users[1]);
stmt.setDouble(3, ((Number) entry.getValue()).doubleValue());
stmt.executeUpdate();
}
}
LoggerDecorator.success(logger, "shipPairs мигрированы");
} catch (SQLException e) {
LoggerDecorator.error(logger, "Ошибка миграции shipPairs", e);
}
}
@SuppressWarnings("unchecked")
private void migrateCustomRoles(Map<String, Object> data) {
Map<String, Object> customRoles = (Map<String, Object>) data.get("custom_roles");
if (customRoles == null) return;
@SuppressWarnings("unchecked")
private void migrateCustomRoles(Map<String, Object> data) {
Map<String, Object> customRoles = (Map<String, Object>) data.get("custom_roles");
if (customRoles == null) return;
logger.info("Миграция custom_roles");
LoggerDecorator.database(logger, "Миграция custom_roles");
try (PreparedStatement stmt = database.prepare(
"INSERT OR REPLACE INTO custom_roles (guild_id, user_id, role_id) VALUES (?, ?, ?)"
)) {
for (Map.Entry<String, Object> guildEntry : customRoles.entrySet()) {
String guildId = guildEntry.getKey();
Map<String, Object> users = (Map<String, Object>) guildEntry.getValue();
try (PreparedStatement stmt =
database.prepare(
"INSERT OR REPLACE INTO custom_roles (guild_id, user_id, role_id) VALUES (?, ?, ?)")) {
for (Map.Entry<String, Object> guildEntry : customRoles.entrySet()) {
String guildId = guildEntry.getKey();
Map<String, Object> users = (Map<String, Object>) guildEntry.getValue();
for (Map.Entry<String, Object> userEntry : users.entrySet()) {
String userId = userEntry.getKey();
Object rolesObj = userEntry.getValue();
for (Map.Entry<String, Object> userEntry : users.entrySet()) {
String userId = userEntry.getKey();
Object rolesObj = userEntry.getValue();
if (rolesObj instanceof java.util.List<?> rolesList) {
for (Object roleId : rolesList) {
stmt.setString(1, guildId);
stmt.setString(2, userId);
stmt.setString(3, roleId.toString());
stmt.executeUpdate();
}
}
}
if (rolesObj instanceof java.util.List<?> rolesList) {
for (Object roleId : rolesList) {
stmt.setString(1, guildId);
stmt.setString(2, userId);
stmt.setString(3, roleId.toString());
stmt.executeUpdate();
}
logger.info("custom_roles мигрированы");
} catch (SQLException e) {
logger.error("Ошибка миграции custom_roles", e);
}
}
}
LoggerDecorator.success(logger, "custom_roles мигрированы");
} catch (SQLException e) {
LoggerDecorator.error(logger, "Ошибка миграции custom_roles", e);
}
}
}

View file

@ -1,249 +1,23 @@
package creator.loki.database;
import creator.loki.database.schema.CoreTables;
import creator.loki.database.schema.FeatureTables;
import creator.loki.util.LoggerDecorator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Схема базы данных - создание таблиц
*/
public class DatabaseSchema {
private static final Logger logger = LoggerFactory.getLogger(DatabaseSchema.class);
private final Database database;
private static final Logger logger = LoggerFactory.getLogger(DatabaseSchema.class);
private final Database database;
public DatabaseSchema(Database database) {
this.database = database;
}
public DatabaseSchema(Database database) {
this.database = database;
}
/**
* Создать все таблицы
*/
public void createTables() {
logger.info("Создание таблиц базы данных...");
createConfigTable();
createUserStatsTable();
createReputationTable();
createShipPairsTable();
createCustomRolesTable();
createVoiceChannelsTable();
createVoicePresetsTable();
createCounterStatsTable();
createRatingsTable();
createWarningsTable();
createModerationCasesTable();
createActiveMutesTable();
createEconomyTable();
createIndexes();
logger.info("Таблицы созданы успешно");
}
private void createConfigTable() {
database.execute("""
CREATE TABLE IF NOT EXISTS config (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
)
""");
}
private void createUserStatsTable() {
database.execute("""
CREATE TABLE IF NOT EXISTS user_stats (
user_id TEXT PRIMARY KEY,
level INTEGER DEFAULT 0,
total_xp INTEGER DEFAULT 0,
message_count INTEGER DEFAULT 0,
voice_minutes INTEGER DEFAULT 0,
last_message_time INTEGER DEFAULT 0,
last_voice_join INTEGER DEFAULT 0
)
""");
}
private void createReputationTable() {
database.execute("""
CREATE TABLE IF NOT EXISTS reputation (
user_id TEXT PRIMARY KEY,
reputation REAL DEFAULT 0.0
)
""");
}
private void createShipPairsTable() {
database.execute("""
CREATE TABLE IF NOT EXISTS ship_pairs (
user1_id TEXT NOT NULL,
user2_id TEXT NOT NULL,
compatibility REAL NOT NULL,
PRIMARY KEY (user1_id, user2_id)
)
""");
}
private void createCustomRolesTable() {
database.execute("""
CREATE TABLE IF NOT EXISTS custom_roles (
guild_id TEXT NOT NULL,
user_id TEXT NOT NULL,
role_id TEXT NOT NULL,
PRIMARY KEY (guild_id, user_id, role_id)
)
""");
}
private void createVoiceChannelsTable() {
database.execute("""
CREATE TABLE IF NOT EXISTS voice_channels (
channel_id TEXT PRIMARY KEY,
owner_id TEXT NOT NULL,
created_at INTEGER NOT NULL,
name TEXT,
user_limit INTEGER DEFAULT 0,
bitrate INTEGER DEFAULT 64000,
locked INTEGER DEFAULT 0,
text_channel_id TEXT
)
""");
}
private void createVoicePresetsTable() {
database.execute("""
CREATE TABLE IF NOT EXISTS voice_presets (
user_id TEXT PRIMARY KEY,
name TEXT,
user_limit INTEGER DEFAULT 0,
bitrate INTEGER DEFAULT 64000,
region TEXT DEFAULT 'auto',
locked INTEGER DEFAULT 0,
allowed_role_id TEXT,
denied_role_id TEXT
)
""");
}
private void createCounterStatsTable() {
database.execute("""
CREATE TABLE IF NOT EXISTS counter_stats (
date TEXT PRIMARY KEY,
joined INTEGER DEFAULT 0,
left_count INTEGER DEFAULT 0,
difference INTEGER DEFAULT 0
)
""");
}
private void createRatingsTable() {
database.execute("""
CREATE TABLE IF NOT EXISTS ratings (
guild_id TEXT NOT NULL,
user_id TEXT NOT NULL,
rating_type TEXT NOT NULL,
value INTEGER NOT NULL,
PRIMARY KEY (guild_id, user_id, rating_type)
)
""");
}
private void createWarningsTable() {
database.execute("""
CREATE TABLE IF NOT EXISTS warnings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
guild_id TEXT NOT NULL,
user_id TEXT NOT NULL,
moderator_id TEXT NOT NULL,
reason TEXT NOT NULL,
created_at INTEGER NOT NULL
)
""");
}
private void createModerationCasesTable() {
database.execute("""
CREATE TABLE IF NOT EXISTS moderation_cases (
id INTEGER PRIMARY KEY AUTOINCREMENT,
guild_id TEXT NOT NULL,
action TEXT NOT NULL,
target_id TEXT NOT NULL,
moderator_id TEXT NOT NULL,
reason TEXT NOT NULL,
created_at INTEGER NOT NULL,
duration_sec INTEGER,
until_ms INTEGER
)
""");
}
private void createActiveMutesTable() {
database.execute("""
CREATE TABLE IF NOT EXISTS active_mutes (
guild_id TEXT NOT NULL,
user_id TEXT NOT NULL,
ends_at_ms INTEGER NOT NULL,
case_id INTEGER NOT NULL,
PRIMARY KEY (guild_id, user_id)
)
""");
}
private void createEconomyTable() {
database.execute("""
CREATE TABLE IF NOT EXISTS economy (
user_id TEXT PRIMARY KEY,
cash INTEGER NOT NULL DEFAULT 0,
bank INTEGER NOT NULL DEFAULT 0,
total_earned INTEGER NOT NULL DEFAULT 0,
total_spent INTEGER NOT NULL DEFAULT 0,
last_daily TEXT,
daily_streak INTEGER DEFAULT 0,
fishing_luck INTEGER DEFAULT 0,
efficiency INTEGER DEFAULT 0,
last_fish TEXT,
job TEXT,
last_work TEXT
)
""");
}
/**
* Создать индексы для ускорения запросов
*/
private void createIndexes() {
logger.info("Создание индексов...");
// Индексы для user_stats
database.execute("CREATE INDEX IF NOT EXISTS idx_user_stats_level ON user_stats(level DESC)");
database.execute("CREATE INDEX IF NOT EXISTS idx_user_stats_total_xp ON user_stats(total_xp DESC)");
database.execute("CREATE INDEX IF NOT EXISTS idx_user_stats_message_count ON user_stats(message_count DESC)");
database.execute("CREATE INDEX IF NOT EXISTS idx_user_stats_voice_minutes ON user_stats(voice_minutes DESC)");
// Индексы для reputation
database.execute("CREATE INDEX IF NOT EXISTS idx_reputation_value ON reputation(reputation DESC)");
// Индексы для ratings
database.execute("CREATE INDEX IF NOT EXISTS idx_ratings_guild_type ON ratings(guild_id, rating_type)");
database.execute("CREATE INDEX IF NOT EXISTS idx_ratings_value ON ratings(value DESC)");
// Индексы для voice_channels
database.execute("CREATE INDEX IF NOT EXISTS idx_voice_channels_owner ON voice_channels(owner_id)");
database.execute("CREATE INDEX IF NOT EXISTS idx_voice_channels_created ON voice_channels(created_at)");
// Индексы для custom_roles
database.execute("CREATE INDEX IF NOT EXISTS idx_custom_roles_guild ON custom_roles(guild_id)");
database.execute("CREATE INDEX IF NOT EXISTS idx_custom_roles_user ON custom_roles(user_id)");
// Индексы для counter_stats
database.execute("CREATE INDEX IF NOT EXISTS idx_counter_stats_date ON counter_stats(date DESC)");
database.execute("CREATE INDEX IF NOT EXISTS idx_warnings_guild_user ON warnings(guild_id, user_id)");
database.execute("CREATE INDEX IF NOT EXISTS idx_warnings_guild_time ON warnings(guild_id, created_at DESC)");
database.execute("CREATE INDEX IF NOT EXISTS idx_mod_cases_guild_id ON moderation_cases(guild_id, id DESC)");
database.execute("CREATE INDEX IF NOT EXISTS idx_active_mutes_ends ON active_mutes(ends_at_ms)");
database.execute("CREATE INDEX IF NOT EXISTS idx_economy_balance ON economy((cash + bank) DESC)");
logger.info("Индексы созданы успешно");
}
public void createTables() {
LoggerDecorator.database(logger, "Создание таблиц базы данных...");
CoreTables.create(database);
FeatureTables.create(database);
LoggerDecorator.success(logger, "Таблицы созданы успешно");
}
}

View file

@ -0,0 +1,73 @@
package creator.loki.database.schema;
import creator.loki.database.Database;
public final class CoreTables {
public static void create(Database database) {
createConfig(database);
createUserStats(database);
createReputation(database);
createShipPairs(database);
createIndexes(database);
}
private static void createConfig(Database database) {
database.execute(
"""
CREATE TABLE IF NOT EXISTS config (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
)
""");
}
private static void createUserStats(Database database) {
database.execute(
"""
CREATE TABLE IF NOT EXISTS user_stats (
user_id TEXT PRIMARY KEY,
level INTEGER DEFAULT 0,
total_xp INTEGER DEFAULT 0,
message_count INTEGER DEFAULT 0,
voice_minutes INTEGER DEFAULT 0,
last_message_time INTEGER DEFAULT 0,
last_voice_join INTEGER DEFAULT 0
)
""");
}
private static void createReputation(Database database) {
database.execute(
"""
CREATE TABLE IF NOT EXISTS reputation (
user_id TEXT PRIMARY KEY,
reputation REAL DEFAULT 0.0
)
""");
}
private static void createShipPairs(Database database) {
database.execute(
"""
CREATE TABLE IF NOT EXISTS ship_pairs (
user1_id TEXT NOT NULL,
user2_id TEXT NOT NULL,
compatibility REAL NOT NULL,
PRIMARY KEY (user1_id, user2_id)
)
""");
}
private static void createIndexes(Database database) {
database.execute("CREATE INDEX IF NOT EXISTS idx_user_stats_level ON user_stats(level DESC)");
database.execute(
"CREATE INDEX IF NOT EXISTS idx_user_stats_total_xp ON user_stats(total_xp DESC)");
database.execute(
"CREATE INDEX IF NOT EXISTS idx_user_stats_message_count ON user_stats(message_count DESC)");
database.execute(
"CREATE INDEX IF NOT EXISTS idx_user_stats_voice_minutes ON user_stats(voice_minutes DESC)");
database.execute(
"CREATE INDEX IF NOT EXISTS idx_reputation_value ON reputation(reputation DESC)");
}
}

View file

@ -0,0 +1,193 @@
package creator.loki.database.schema;
import creator.loki.database.Database;
public final class FeatureTables {
public static void create(Database database) {
createEconomy(database);
createMusicQueue(database);
createWarnings(database);
createModerationCases(database);
createActiveMutes(database);
createCustomRoles(database);
createVoiceChannels(database);
createVoicePresets(database);
createCounterStats(database);
createRatings(database);
createIndexes(database);
}
private static void createEconomy(Database database) {
database.execute(
"""
CREATE TABLE IF NOT EXISTS economy (
user_id TEXT PRIMARY KEY,
cash INTEGER NOT NULL DEFAULT 0,
bank INTEGER NOT NULL DEFAULT 0,
total_earned INTEGER NOT NULL DEFAULT 0,
total_spent INTEGER NOT NULL DEFAULT 0,
last_daily TEXT,
daily_streak INTEGER DEFAULT 0,
fishing_luck INTEGER DEFAULT 0,
efficiency INTEGER DEFAULT 0,
last_fish TEXT,
job TEXT,
last_work TEXT
)
""");
}
private static void createMusicQueue(Database database) {
database.execute(
"""
CREATE TABLE IF NOT EXISTS music_queue (
guild_id TEXT NOT NULL,
position INTEGER NOT NULL,
track_encoded TEXT NOT NULL,
track_title TEXT,
track_author TEXT,
track_uri TEXT,
track_duration BIGINT,
PRIMARY KEY (guild_id, position)
)
""");
}
private static void createWarnings(Database database) {
database.execute(
"""
CREATE TABLE IF NOT EXISTS warnings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
guild_id TEXT NOT NULL,
user_id TEXT NOT NULL,
moderator_id TEXT NOT NULL,
reason TEXT NOT NULL,
created_at INTEGER NOT NULL
)
""");
}
private static void createModerationCases(Database database) {
database.execute(
"""
CREATE TABLE IF NOT EXISTS moderation_cases (
id INTEGER PRIMARY KEY AUTOINCREMENT,
guild_id TEXT NOT NULL,
action TEXT NOT NULL,
target_id TEXT NOT NULL,
moderator_id TEXT NOT NULL,
reason TEXT NOT NULL,
created_at INTEGER NOT NULL,
duration_sec INTEGER,
until_ms INTEGER
)
""");
}
private static void createActiveMutes(Database database) {
database.execute(
"""
CREATE TABLE IF NOT EXISTS active_mutes (
guild_id TEXT NOT NULL,
user_id TEXT NOT NULL,
ends_at_ms INTEGER NOT NULL,
case_id INTEGER NOT NULL,
PRIMARY KEY (guild_id, user_id)
)
""");
}
private static void createCustomRoles(Database database) {
database.execute(
"""
CREATE TABLE IF NOT EXISTS custom_roles (
guild_id TEXT NOT NULL,
user_id TEXT NOT NULL,
role_id TEXT NOT NULL,
PRIMARY KEY (guild_id, user_id, role_id)
)
""");
}
private static void createVoiceChannels(Database database) {
database.execute(
"""
CREATE TABLE IF NOT EXISTS voice_channels (
channel_id TEXT PRIMARY KEY,
owner_id TEXT NOT NULL,
created_at INTEGER NOT NULL,
name TEXT,
user_limit INTEGER DEFAULT 0,
bitrate INTEGER DEFAULT 64000,
locked INTEGER DEFAULT 0,
text_channel_id TEXT
)
""");
}
private static void createVoicePresets(Database database) {
database.execute(
"""
CREATE TABLE IF NOT EXISTS voice_presets (
user_id TEXT PRIMARY KEY,
name TEXT,
user_limit INTEGER DEFAULT 0,
bitrate INTEGER DEFAULT 64000,
region TEXT DEFAULT 'auto',
locked INTEGER DEFAULT 0,
allowed_role_id TEXT,
denied_role_id TEXT
)
""");
}
private static void createCounterStats(Database database) {
database.execute(
"""
CREATE TABLE IF NOT EXISTS counter_stats (
date TEXT PRIMARY KEY,
joined INTEGER DEFAULT 0,
left_count INTEGER DEFAULT 0,
difference INTEGER DEFAULT 0
)
""");
}
private static void createRatings(Database database) {
database.execute(
"""
CREATE TABLE IF NOT EXISTS ratings (
guild_id TEXT NOT NULL,
user_id TEXT NOT NULL,
rating_type TEXT NOT NULL,
value INTEGER NOT NULL,
PRIMARY KEY (guild_id, user_id, rating_type)
)
""");
}
private static void createIndexes(Database database) {
database.execute(
"CREATE INDEX IF NOT EXISTS idx_ratings_guild_type ON ratings(guild_id, rating_type)");
database.execute("CREATE INDEX IF NOT EXISTS idx_ratings_value ON ratings(value DESC)");
database.execute(
"CREATE INDEX IF NOT EXISTS idx_voice_channels_owner ON voice_channels(owner_id)");
database.execute(
"CREATE INDEX IF NOT EXISTS idx_voice_channels_created ON voice_channels(created_at)");
database.execute("CREATE INDEX IF NOT EXISTS idx_custom_roles_guild ON custom_roles(guild_id)");
database.execute("CREATE INDEX IF NOT EXISTS idx_custom_roles_user ON custom_roles(user_id)");
database.execute(
"CREATE INDEX IF NOT EXISTS idx_counter_stats_date ON counter_stats(date DESC)");
database.execute(
"CREATE INDEX IF NOT EXISTS idx_warnings_guild_user ON warnings(guild_id, user_id)");
database.execute(
"CREATE INDEX IF NOT EXISTS idx_warnings_guild_time ON warnings(guild_id, created_at DESC)");
database.execute(
"CREATE INDEX IF NOT EXISTS idx_mod_cases_guild_id ON moderation_cases(guild_id, id DESC)");
database.execute(
"CREATE INDEX IF NOT EXISTS idx_active_mutes_ends ON active_mutes(ends_at_ms)");
database.execute(
"CREATE INDEX IF NOT EXISTS idx_economy_balance ON economy((cash + bank) DESC)");
}
}

View file

@ -19,120 +19,131 @@ import creator.loki.database.Database;
import creator.loki.database.DatabaseMigration;
import creator.loki.database.DatabaseSchema;
import creator.loki.feature.config.repository.ConfigRepository;
import creator.loki.feature.config.repository.GuildConfigRepository;
import creator.loki.feature.config.repository.PrefixedGuildConfigRepository;
import creator.loki.feature.rating.repository.RatingsRepository;
import creator.loki.feature.reputation.repository.ReputationRepository;
import creator.loki.feature.ship.repository.ShipPairRepository;
import creator.loki.feature.counter.CounterModule;
import creator.loki.feature.economy.EconomyCommandRegistrar;
import creator.loki.feature.economy.EconomyModule;
import creator.loki.feature.entertainment.EntertainmentCommandRegistrar;
import creator.loki.feature.level.LevelCommandRegistrar;
import creator.loki.feature.level.LevelModule;
import creator.loki.feature.metrics.MetricsCollector;
import creator.loki.feature.metrics.HealthHttpServer;
import creator.loki.feature.metrics.MetricsCollector;
import creator.loki.feature.metrics.MetricsModule;
import creator.loki.feature.moderation.ModerationCommandRegistrar;
import creator.loki.feature.moderation.ModerationModule;
import creator.loki.feature.profile.ProfileModule;
import creator.loki.feature.role.RoleModule;
import creator.loki.feature.utility.UtilityCommandRegistrar;
import creator.loki.feature.music.MusicModule;
import creator.loki.feature.music.MusicCommandRegistrar;
import creator.loki.feature.music.MusicModule;
import creator.loki.feature.music.repository.QueueRepository;
import creator.loki.feature.profile.ProfileModule;
import creator.loki.feature.radio.RadioCommandRegistrar;
import creator.loki.feature.radio.RadioModule;
import creator.loki.feature.rating.repository.RatingsRepository;
import creator.loki.feature.reputation.repository.ReputationRepository;
import creator.loki.feature.role.RoleModule;
import creator.loki.feature.ship.repository.ShipPairRepository;
import creator.loki.feature.utility.UtilityCommandRegistrar;
import creator.loki.feature.voice.VoiceModule;
import creator.loki.feature.welcome.WelcomeModule;
import creator.loki.feature.config.repository.GuildConfigRepository;
import creator.loki.feature.economy.EconomyCommandRegistrar;
import creator.loki.feature.economy.EconomyModule;
public class BotModule extends AbstractModule {
@Override
protected void configure() {
bind(CommandRegistry.class).in(Singleton.class);
bind(CooldownManager.class).in(Singleton.class);
bind(RateLimiter.class).in(Singleton.class);
bind(MetricsCollector.class).in(Singleton.class);
bind(HealthHttpServer.class).in(Singleton.class);
bind(ListenerAggregator.class).in(Singleton.class);
bind(MessageCache.class).in(Singleton.class);
bind(ShutdownManager.class).in(Singleton.class);
bind(PrefixCommandRegistrar.class).in(Singleton.class);
@Override
protected void configure() {
bind(CommandRegistry.class).in(Singleton.class);
bind(CooldownManager.class).in(Singleton.class);
bind(RateLimiter.class).in(Singleton.class);
bind(MetricsCollector.class).in(Singleton.class);
bind(HealthHttpServer.class).in(Singleton.class);
bind(ListenerAggregator.class).in(Singleton.class);
bind(MessageCache.class).in(Singleton.class);
bind(ShutdownManager.class).in(Singleton.class);
bind(PrefixCommandRegistrar.class).in(Singleton.class);
install(new VoiceModule());
install(new MusicModule());
install(new LevelModule());
install(new ModerationModule());
install(new RoleModule());
install(new CounterModule());
install(new ProfileModule());
install(new MetricsModule());
install(new WelcomeModule());
install(new EconomyModule());
install(new VoiceModule());
install(new MusicModule());
install(new LevelModule());
install(new ModerationModule());
install(new RoleModule());
install(new CounterModule());
install(new ProfileModule());
install(new MetricsModule());
install(new WelcomeModule());
install(new EconomyModule());
install(new RadioModule());
Multibinder<CommandRegistrar> commandBinder = Multibinder.newSetBinder(binder(), CommandRegistrar.class);
commandBinder.addBinding().to(LevelCommandRegistrar.class);
commandBinder.addBinding().to(ModerationCommandRegistrar.class);
commandBinder.addBinding().to(EntertainmentCommandRegistrar.class);
commandBinder.addBinding().to(UtilityCommandRegistrar.class);
commandBinder.addBinding().to(MusicCommandRegistrar.class);
commandBinder.addBinding().to(EconomyCommandRegistrar.class);
}
Multibinder<CommandRegistrar> commandBinder =
Multibinder.newSetBinder(binder(), CommandRegistrar.class);
commandBinder.addBinding().to(LevelCommandRegistrar.class);
commandBinder.addBinding().to(ModerationCommandRegistrar.class);
commandBinder.addBinding().to(EntertainmentCommandRegistrar.class);
commandBinder.addBinding().to(UtilityCommandRegistrar.class);
commandBinder.addBinding().to(MusicCommandRegistrar.class);
commandBinder.addBinding().to(EconomyCommandRegistrar.class);
commandBinder.addBinding().to(RadioCommandRegistrar.class);
}
@Provides
@Singleton
BotConfig provideBotConfig() {
return ConfigLoader.load();
}
@Provides
@Singleton
BotConfig provideBotConfig() {
return ConfigLoader.load();
}
@Provides
@Singleton
EventDispatcher provideEventDispatcher(
BotConfig config,
CommandRegistry registry,
RateLimiter rateLimiter,
MetricsCollector metrics
) {
return new EventDispatcher(config, registry, rateLimiter, metrics);
}
@Provides
@Singleton
EventDispatcher provideEventDispatcher(
BotConfig config,
CommandRegistry registry,
RateLimiter rateLimiter,
MetricsCollector metrics) {
return new EventDispatcher(config, registry, rateLimiter, metrics);
}
@Provides
@Singleton
Database provideDatabase(BotConfig config) {
Database database = new Database(config.getDbPath());
database.connect();
DatabaseSchema schema = new DatabaseSchema(database);
schema.createTables();
DatabaseMigration migration = new DatabaseMigration(database);
migration.migrateFromJson("data/bot_data.json");
return database;
}
@Provides
@Singleton
Database provideDatabase(BotConfig config) {
Database database = new Database(config.getDbPath());
database.connect();
DatabaseSchema schema = new DatabaseSchema(database);
schema.createTables();
DatabaseMigration migration = new DatabaseMigration(database);
migration.migrateFromJson("data/bot_data.json");
return database;
}
@Provides
@Singleton
ConfigRepository provideConfigRepository(Database database) {
return new ConfigRepository(database);
}
@Provides
@Singleton
ConfigRepository provideConfigRepository(Database database) {
return new ConfigRepository(database);
}
@Provides
@Singleton
GuildConfigRepository provideGuildConfigRepository(ConfigRepository configRepository) {
return new PrefixedGuildConfigRepository(configRepository);
}
@Provides
@Singleton
GuildConfigRepository provideGuildConfigRepository(ConfigRepository configRepository) {
return new PrefixedGuildConfigRepository(configRepository);
}
@Provides
@Singleton
RatingsRepository provideRatingsRepository(Database database) {
return new RatingsRepository(database);
}
@Provides
@Singleton
RatingsRepository provideRatingsRepository(Database database) {
return new RatingsRepository(database);
}
@Provides
@Singleton
ReputationRepository provideReputationRepository(Database database) {
return new ReputationRepository(database);
}
@Provides
@Singleton
ReputationRepository provideReputationRepository(Database database) {
return new ReputationRepository(database);
}
@Provides
@Singleton
ShipPairRepository provideShipPairRepository(Database database) {
return new ShipPairRepository(database);
}
@Provides
@Singleton
ShipPairRepository provideShipPairRepository(Database database) {
return new ShipPairRepository(database);
}
@Provides
@Singleton
QueueRepository provideQueueRepository(Database database) {
return new QueueRepository(database);
}
}

View file

@ -1,79 +1,79 @@
package creator.loki.embed;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.MessageEmbed;
import java.awt.Color;
import java.time.Instant;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.MessageEmbed;
public class EmbedFactory {
private final EmbedBuilder builder;
private final EmbedBuilder builder;
private EmbedFactory() {
this.builder = new EmbedBuilder();
}
private EmbedFactory() {
this.builder = new EmbedBuilder();
}
public static EmbedFactory create() {
return new EmbedFactory();
}
public static EmbedFactory create() {
return new EmbedFactory();
}
public EmbedFactory withTitle(String title) {
builder.setTitle(title);
return this;
}
public EmbedFactory withTitle(String title) {
builder.setTitle(title);
return this;
}
public EmbedFactory withDescription(String description) {
builder.setDescription(description);
return this;
}
public EmbedFactory withDescription(String description) {
builder.setDescription(description);
return this;
}
public EmbedFactory withColor(Color color) {
builder.setColor(color);
return this;
}
public EmbedFactory withColor(Color color) {
builder.setColor(color);
return this;
}
public EmbedFactory addField(String name, String value, boolean inline) {
builder.addField(name, value, inline);
return this;
}
public EmbedFactory addField(String name, String value, boolean inline) {
builder.addField(name, value, inline);
return this;
}
public EmbedFactory withFooter(String text) {
builder.setFooter(text);
return this;
}
public EmbedFactory withFooter(String text) {
builder.setFooter(text);
return this;
}
public EmbedFactory withFooter(String text, String iconUrl) {
builder.setFooter(text, iconUrl);
return this;
}
public EmbedFactory withFooter(String text, String iconUrl) {
builder.setFooter(text, iconUrl);
return this;
}
public EmbedFactory withThumbnail(String url) {
builder.setThumbnail(url);
return this;
}
public EmbedFactory withThumbnail(String url) {
builder.setThumbnail(url);
return this;
}
public EmbedFactory withImage(String url) {
builder.setImage(url);
return this;
}
public EmbedFactory withImage(String url) {
builder.setImage(url);
return this;
}
public EmbedFactory withAuthor(String name) {
builder.setAuthor(name);
return this;
}
public EmbedFactory withAuthor(String name) {
builder.setAuthor(name);
return this;
}
public EmbedFactory withAuthor(String name, String url) {
builder.setAuthor(name, url);
return this;
}
public EmbedFactory withAuthor(String name, String url) {
builder.setAuthor(name, url);
return this;
}
public EmbedFactory withAuthor(String name, String url, String iconUrl) {
builder.setAuthor(name, url, iconUrl);
return this;
}
public EmbedFactory withAuthor(String name, String url, String iconUrl) {
builder.setAuthor(name, url, iconUrl);
return this;
}
public MessageEmbed build() {
builder.setTimestamp(Instant.now());
return builder.build();
}
public MessageEmbed build() {
builder.setTimestamp(Instant.now());
return builder.build();
}
}

View file

@ -1,50 +1,50 @@
package creator.loki.embed;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.MessageEmbed;
import java.awt.Color;
import java.time.Instant;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.MessageEmbed;
public final class EmbedTemplates {
private EmbedTemplates() {
// Запрет создания экземпляров
}
private EmbedTemplates() {
// Запрет создания экземпляров
}
public static MessageEmbed success(String title, String description) {
return new EmbedBuilder()
.setColor(Color.GREEN)
.setTitle("✅ " + title)
.setDescription(description)
.setTimestamp(Instant.now())
.build();
}
public static MessageEmbed success(String title, String description) {
return new EmbedBuilder()
.setColor(Color.GREEN)
.setTitle("✅ " + title)
.setDescription(description)
.setTimestamp(Instant.now())
.build();
}
public static MessageEmbed error(String title, String description) {
return new EmbedBuilder()
.setColor(Color.RED)
.setTitle("❌ " + title)
.setDescription(description)
.setTimestamp(Instant.now())
.build();
}
public static MessageEmbed error(String title, String description) {
return new EmbedBuilder()
.setColor(Color.RED)
.setTitle("❌ " + title)
.setDescription(description)
.setTimestamp(Instant.now())
.build();
}
public static MessageEmbed info(String title, String description) {
return new EmbedBuilder()
.setColor(Color.CYAN)
.setTitle("ℹ️ " + title)
.setDescription(description)
.setTimestamp(Instant.now())
.build();
}
public static MessageEmbed info(String title, String description) {
return new EmbedBuilder()
.setColor(Color.CYAN)
.setTitle("ℹ️ " + title)
.setDescription(description)
.setTimestamp(Instant.now())
.build();
}
public static MessageEmbed warning(String title, String description) {
return new EmbedBuilder()
.setColor(Color.ORANGE)
.setTitle("⚠️ " + title)
.setDescription(description)
.setTimestamp(Instant.now())
.build();
}
public static MessageEmbed warning(String title, String description) {
return new EmbedBuilder()
.setColor(Color.ORANGE)
.setTitle("⚠️ " + title)
.setDescription(description)
.setTimestamp(Instant.now())
.build();
}
}

View file

@ -1,17 +1,18 @@
package creator.loki.embed;
import creator.loki.command.CommandCategories;
import java.awt.Color;
import java.util.Map;
public final class HelpCategories {
private static final Map<String, String> EMOJIS;
private static final Map<String, Color> COLORS;
private static final Map<String, String> DESCRIPTIONS;
import creator.loki.command.CommandCategories;
static {
EMOJIS = Map.ofEntries(
public final class HelpCategories {
private static final Map<String, String> EMOJIS;
private static final Map<String, Color> COLORS;
private static final Map<String, String> DESCRIPTIONS;
static {
EMOJIS =
Map.ofEntries(
Map.entry("Общие", "📋"),
Map.entry("Роли", "🎭"),
Map.entry("Развлечения", "🎮"),
@ -21,10 +22,10 @@ public final class HelpCategories {
Map.entry("Утилиты", "🔧"),
Map.entry("Система", "💠"),
Map.entry("Настройка", "⚙️"),
Map.entry("Супер-Админ", "🔐")
);
Map.entry("Супер-Админ", "🔐"));
COLORS = Map.ofEntries(
COLORS =
Map.ofEntries(
Map.entry("Общие", new Color(52, 152, 219)),
Map.entry("Роли", new Color(155, 89, 182)),
Map.entry("Развлечения", new Color(241, 196, 15)),
@ -34,42 +35,41 @@ public final class HelpCategories {
Map.entry("Утилиты", new Color(52, 152, 219)),
Map.entry("Система", new Color(52, 73, 94)),
Map.entry("Настройка", new Color(149, 165, 166)),
Map.entry("Супер-Админ", new Color(113, 54, 138))
);
Map.entry("Супер-Админ", new Color(113, 54, 138)));
DESCRIPTIONS = Map.ofEntries(
DESCRIPTIONS =
Map.ofEntries(
Map.entry("Общие", "Основные команды бота"),
Map.entry("Роли", "Управление кастомными ролями"),
Map.entry("Развлечения", "Игры, мемы, рейтинги, текст"),
Map.entry(CommandCategories.GIF_INTERACTIVE, "Действия с участником в GIF (nekos.life)"),
Map.entry(
CommandCategories.GIF_INTERACTIVE, "Действия с участником в GIF (nekos.life)"),
Map.entry("Модерация", "Команды модерации сервера"),
Map.entry("Статистика", "Уровни, репутация и статистика"),
Map.entry("Утилиты", "Полезные инструменты"),
Map.entry("Система", "Метрики и служебная информация для админов"),
Map.entry("Настройка", "Настройка систем бота (только админы)"),
Map.entry("Супер-Админ", "Сервисные команды владельца бота")
);
}
Map.entry("Супер-Админ", "Сервисные команды владельца бота"));
}
private HelpCategories() {
}
private HelpCategories() {}
public static String getEmoji(String category) {
return EMOJIS.getOrDefault(category, "📌");
}
public static String getEmoji(String category) {
return EMOJIS.getOrDefault(category, "📌");
}
public static Color getColor(String category) {
return COLORS.getOrDefault(category, new Color(88, 101, 242));
}
public static Color getColor(String category) {
return COLORS.getOrDefault(category, new Color(88, 101, 242));
}
public static String getDescription(String category) {
return DESCRIPTIONS.getOrDefault(category, "Команды категории");
}
public static String getDescription(String category) {
return DESCRIPTIONS.getOrDefault(category, "Команды категории");
}
public static boolean isCategoryVisible(String category, boolean isAdmin, boolean isSuper) {
if ("Настройка".equals(category) && !isAdmin) {
return false;
}
return !"Супер-Админ".equals(category) || isSuper;
public static boolean isCategoryVisible(String category, boolean isAdmin, boolean isSuper) {
if ("Настройка".equals(category) && !isAdmin) {
return false;
}
return !"Супер-Админ".equals(category) || isSuper;
}
}

View file

@ -1,158 +1,172 @@
package creator.loki.feature.admin.commands;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.config.BotConfig;
import creator.loki.feature.rating.repository.RatingsRepository;
import creator.loki.embed.EmbedFactory;
import net.dv8tion.jda.api.entities.Member;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.Color;
import java.util.Random;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.config.BotConfig;
import creator.loki.embed.EmbedFactory;
import creator.loki.feature.rating.repository.RatingsRepository;
import net.dv8tion.jda.api.entities.Member;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Команда для генерации всех rating значений для всех участников сервера
* Доступна только супер-админу
* Команда для генерации всех rating значений для всех участников сервера Доступна только
* супер-админу
*/
public class GenerateRatingsCommand implements Command {
private static final Logger logger = LoggerFactory.getLogger(GenerateRatingsCommand.class);
private final RatingsRepository ratingsRepository;
private final BotConfig config;
private final Random random = new Random();
private static final Logger logger = LoggerFactory.getLogger(GenerateRatingsCommand.class);
private final RatingsRepository ratingsRepository;
private final BotConfig config;
private final Random random = new Random();
public GenerateRatingsCommand(RatingsRepository ratingsRepository, BotConfig config) {
this.ratingsRepository = ratingsRepository;
this.config = config;
public GenerateRatingsCommand(RatingsRepository ratingsRepository, BotConfig config) {
this.ratingsRepository = ratingsRepository;
this.config = config;
}
@Override
public String getName() {
return "generateratings";
}
@Override
public String getDescription() {
return "Генерирует все rating значения для всех участников сервера (только для супер-админа)";
}
@Override
public String getUsage() {
return "generateratings";
}
@Override
public String getCategory() {
return "Супер-Админ";
}
@Override
public void execute(CommandContext context) {
if (!config.getSuperAdminId().equals(context.getAuthor().getUser().getId())) {
context
.getChannel()
.sendMessageEmbeds(
EmbedFactory.create()
.withTitle("❌ Недостаточно прав")
.withDescription("Эта команда доступна только супер-администратору бота.")
.withColor(Color.RED)
.build())
.queue();
return;
}
@Override
public String getName() {
return "generateratings";
if (context.getGuild() == null) {
context.getChannel().sendMessage("❌ Команда работает только на сервере.").queue();
return;
}
@Override
public String getDescription() {
return "Генерирует все rating значения для всех участников сервера (только для супер-админа)";
}
@Override
public String getUsage() {
return "generateratings";
}
@Override
public String getCategory() {
return "Супер-Админ";
}
@Override
public void execute(CommandContext context) {
if (!config.getSuperAdminId().equals(context.getAuthor().getUser().getId())) {
context.getChannel().sendMessageEmbeds(
var initial =
context
.getChannel()
.sendMessageEmbeds(
EmbedFactory.create()
.withTitle("❌ Недостаточно прав")
.withDescription("Эта команда доступна только супер-администратору бота.")
.withColor(Color.RED)
.build()
).queue();
return;
}
if (context.getGuild() == null) {
context.getChannel().sendMessage("❌ Команда работает только на сервере.").queue();
return;
}
var initial = context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.withTitle("⏳ Генерация рейтингов...")
.withDescription("Начинаю генерацию всех rating значений для участников сервера...")
.withColor(Color.YELLOW)
.build()
).complete();
try {
String guildId = context.getGuild().getId();
int memberCount = 0;
int generatedCount = 0;
for (Member member : context.getGuild().getMembers()) {
if (member.getUser().isBot()) {
continue;
}
memberCount++;
String userId = member.getUser().getId();
generateRatingForUser(guildId, userId, "ship");
generateRatingForUser(guildId, userId, "gay");
generateRatingForUser(guildId, userId, "iq");
generateRatingForUser(guildId, userId, "pp");
generateRatingForUser(guildId, userId, "mustache");
generateRatingForUser(guildId, userId, "fat");
generateRatingForUser(guildId, userId, "boykisser");
generatedCount++;
}
initial.editMessageEmbeds(
EmbedFactory.create()
.withTitle("✅ Генерация завершена")
.withTitle("⏳ Генерация рейтингов...")
.withDescription(
"Успешно сгенерированы все rating значения!\n\n" +
"**Статистика:**\n" +
"• Обработано участников: **" + memberCount + "**\n" +
"• Сгенерировано значений: **" + (generatedCount * 7) + "**\n" +
"• Типы рейтингов: ship, gay, iq, pp, mustache, fat, boykisser"
)
.withColor(Color.GREEN)
.withFooter("Инициатор: " + context.getAuthor().getUser().getName(), context.getAuthor().getAvatarUrl())
.build()
).queue();
"Начинаю генерацию всех rating значений для участников сервера...")
.withColor(Color.YELLOW)
.build())
.complete();
logger.info("Супер-админ {} сгенерировал рейтинги для {} участников",
context.getAuthor().getUser().getName(), memberCount);
try {
String guildId = context.getGuild().getId();
int memberCount = 0;
int generatedCount = 0;
} catch (Exception e) {
logger.error("Ошибка при генерации рейтингов", e);
initial.editMessageEmbeds(
EmbedFactory.create()
.withTitle("❌ Ошибка")
.withDescription("Произошла ошибка при генерации рейтингов: " + e.getMessage())
.withColor(Color.RED)
.build()
).queue();
for (Member member : context.getGuild().getMembers()) {
if (member.getUser().isBot()) {
continue;
}
memberCount++;
String userId = member.getUser().getId();
generateRatingForUser(guildId, userId, "ship");
generateRatingForUser(guildId, userId, "gay");
generateRatingForUser(guildId, userId, "iq");
generateRatingForUser(guildId, userId, "pp");
generateRatingForUser(guildId, userId, "mustache");
generateRatingForUser(guildId, userId, "fat");
generateRatingForUser(guildId, userId, "boykisser");
generatedCount++;
}
initial
.editMessageEmbeds(
EmbedFactory.create()
.withTitle("✅ Генерация завершена")
.withDescription(
"Успешно сгенерированы все rating значения!\n\n"
+ "**Статистика:**\n"
+ "• Обработано участников: **"
+ memberCount
+ "**\n"
+ "• Сгенерировано значений: **"
+ (generatedCount * 7)
+ "**\n"
+ "• Типы рейтингов: ship, gay, iq, pp, mustache, fat, boykisser")
.withColor(Color.GREEN)
.withFooter(
"Инициатор: " + context.getAuthor().getUser().getName(),
context.getAuthor().getAvatarUrl())
.build())
.queue();
logger.info(
"Супер-админ {} сгенерировал рейтинги для {} участников",
context.getAuthor().getUser().getName(),
memberCount);
} catch (Exception e) {
logger.error("Ошибка при генерации рейтингов", e);
initial
.editMessageEmbeds(
EmbedFactory.create()
.withTitle("❌ Ошибка")
.withDescription("Произошла ошибка при генерации рейтингов: " + e.getMessage())
.withColor(Color.RED)
.build())
.queue();
}
}
/** Генерирует rating значение для пользователя */
private void generateRatingForUser(String guildId, String userId, String ratingType) {
// Проверяем существует ли уже значение
if (ratingsRepository.hasRating(guildId, userId, ratingType)) {
return; // Не перезаписываем существующие значения
}
/**
* Генерирует rating значение для пользователя
*/
private void generateRatingForUser(String guildId, String userId, String ratingType) {
// Проверяем существует ли уже значение
if (ratingsRepository.hasRating(guildId, userId, ratingType)) {
return; // Не перезаписываем существующие значения
}
// Генерируем значение в зависимости от типа
int value = switch (ratingType) {
case "iq" -> random.nextInt(201); // 0-200
case "pp" -> random.nextInt(31); // 0-30 см
case "mustache" -> {
// Редкий шанс на легендарные усы
if (random.nextInt(100) < 1) {
yield 1000 + random.nextInt(500); // 1000-1500
}
yield random.nextInt(101); // 0-100
// Генерируем значение в зависимости от типа
int value =
switch (ratingType) {
case "iq" -> random.nextInt(201); // 0-200
case "pp" -> random.nextInt(31); // 0-30 см
case "mustache" -> {
// Редкий шанс на легендарные усы
if (random.nextInt(100) < 1) {
yield 1000 + random.nextInt(500); // 1000-1500
}
case "fat" -> random.nextInt(101); // 0-100%
case "boykisser" -> random.nextInt(101); // 0-100%
default -> random.nextInt(101); // ship, gay: 0-100%
yield random.nextInt(101); // 0-100
}
case "fat" -> random.nextInt(101); // 0-100%
case "boykisser" -> random.nextInt(101); // 0-100%
default -> random.nextInt(101); // ship, gay: 0-100%
};
// Сохраняем значение в БД
ratingsRepository.setRating(guildId, userId, ratingType, value);
}
// Сохраняем значение в БД
ratingsRepository.setRating(guildId, userId, ratingType, value);
}
}

View file

@ -1,84 +1,74 @@
package creator.loki.feature.config.repository;
import creator.loki.database.Database;
import creator.loki.feature.config.repository.StringConfigStore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
/**
* Репозиторий для работы с конфигурацией
*/
import creator.loki.database.Database;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** Репозиторий для работы с конфигурацией */
public class ConfigRepository implements StringConfigStore {
private static final Logger logger = LoggerFactory.getLogger(ConfigRepository.class);
private final Database database;
private static final Logger logger = LoggerFactory.getLogger(ConfigRepository.class);
private final Database database;
public ConfigRepository(Database database) {
this.database = database;
public ConfigRepository(Database database) {
this.database = database;
}
/** Получить значение конфигурации */
public String get(String key) {
try (PreparedStatement stmt = database.prepare("SELECT value FROM config WHERE key = ?")) {
stmt.setString(1, key);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
return rs.getString("value");
}
return null;
} catch (SQLException e) {
logger.error("Ошибка получения конфигурации: {}", key, e);
return null;
}
}
/**
* Получить значение конфигурации
*/
public String get(String key) {
try (PreparedStatement stmt = database.prepare("SELECT value FROM config WHERE key = ?")) {
stmt.setString(1, key);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
return rs.getString("value");
}
return null;
} catch (SQLException e) {
logger.error("Ошибка получения конфигурации: {}", key, e);
return null;
}
/** Установить значение конфигурации */
public void set(String key, String value) {
try (PreparedStatement stmt =
database.prepare("INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)")) {
stmt.setString(1, key);
stmt.setString(2, value);
stmt.executeUpdate();
logger.info("Конфигурация обновлена: {} = {}", key, value);
} catch (SQLException e) {
logger.error("Ошибка сохранения конфигурации: {}", key, e);
}
}
/**
* Установить значение конфигурации
*/
public void set(String key, String value) {
try (PreparedStatement stmt = database.prepare(
"INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)"
)) {
stmt.setString(1, key);
stmt.setString(2, value);
stmt.executeUpdate();
logger.info("Конфигурация обновлена: {} = {}", key, value);
} catch (SQLException e) {
logger.error("Ошибка сохранения конфигурации: {}", key, e);
}
public Map<String, String> getAll() {
Map<String, String> result = new HashMap<>();
try (PreparedStatement stmt = database.prepare("SELECT key, value FROM config")) {
ResultSet rs = stmt.executeQuery();
while (rs.next()) {
result.put(rs.getString("key"), rs.getString("value"));
}
} catch (SQLException e) {
logger.error("Ошибка получения всех конфигураций", e);
}
return result;
}
public Map<String, String> getAll() {
Map<String, String> result = new HashMap<>();
try (PreparedStatement stmt = database.prepare("SELECT key, value FROM config")) {
ResultSet rs = stmt.executeQuery();
while (rs.next()) {
result.put(rs.getString("key"), rs.getString("value"));
}
} catch (SQLException e) {
logger.error("Ошибка получения всех конфигураций", e);
}
return result;
}
/**
* Удалить значение конфигурации
*/
public void delete(String key) {
try (PreparedStatement stmt = database.prepare("DELETE FROM config WHERE key = ?")) {
stmt.setString(1, key);
stmt.executeUpdate();
logger.info("Конфигурация удалена: {}", key);
} catch (SQLException e) {
logger.error("Ошибка удаления конфигурации: {}", key, e);
}
/** Удалить значение конфигурации */
public void delete(String key) {
try (PreparedStatement stmt = database.prepare("DELETE FROM config WHERE key = ?")) {
stmt.setString(1, key);
stmt.executeUpdate();
logger.info("Конфигурация удалена: {}", key);
} catch (SQLException e) {
logger.error("Ошибка удаления конфигурации: {}", key, e);
}
}
}

View file

@ -1,16 +1,17 @@
package creator.loki.feature.config.repository;
/**
* Ключи конфигурации с привязкой к гильдии (хранение: {@code guildId + ":" + key} в общей таблице config).
* Ключи конфигурации с привязкой к гильдии (хранение: {@code guildId + ":" + key} в общей таблице
* config).
*/
public interface GuildConfigRepository {
/**
* @return значение или {@code null}, если нет записи
*/
String get(String guildId, String key);
/**
* @return значение или {@code null}, если нет записи
*/
String get(String guildId, String key);
void set(String guildId, String key, String value);
void set(String guildId, String key, String value);
void delete(String guildId, String key);
void delete(String guildId, String key);
}

View file

@ -1,35 +1,33 @@
package creator.loki.feature.config.repository;
import creator.loki.feature.config.repository.GuildConfigRepository;
import creator.loki.feature.config.repository.StringConfigStore;
/**
* Реализация {@link GuildConfigRepository} поверх {@link StringConfigStore} с префиксом {@code guildId:}.
* Реализация {@link GuildConfigRepository} поверх {@link StringConfigStore} с префиксом {@code
* guildId:}.
*/
public final class PrefixedGuildConfigRepository implements GuildConfigRepository {
private final StringConfigStore store;
private final StringConfigStore store;
public PrefixedGuildConfigRepository(StringConfigStore store) {
this.store = store;
}
public PrefixedGuildConfigRepository(StringConfigStore store) {
this.store = store;
}
private static String storageKey(String guildId, String key) {
return guildId + ":" + key;
}
private static String storageKey(String guildId, String key) {
return guildId + ":" + key;
}
@Override
public String get(String guildId, String key) {
return store.get(storageKey(guildId, key));
}
@Override
public String get(String guildId, String key) {
return store.get(storageKey(guildId, key));
}
@Override
public void set(String guildId, String key, String value) {
store.set(storageKey(guildId, key), value);
}
@Override
public void set(String guildId, String key, String value) {
store.set(storageKey(guildId, key), value);
}
@Override
public void delete(String guildId, String key) {
store.delete(storageKey(guildId, key));
}
@Override
public void delete(String guildId, String key) {
store.delete(storageKey(guildId, key));
}
}

View file

@ -1,16 +1,14 @@
package creator.loki.feature.config.repository;
/**
* Плоское хранилище строковых ключей (глобальная таблица config и т.п.).
*/
/** Плоское хранилище строковых ключей (глобальная таблица config и т.п.). */
public interface StringConfigStore {
/**
* @return значение или {@code null}
*/
String get(String key);
/**
* @return значение или {@code null}
*/
String get(String key);
void set(String key, String value);
void set(String key, String value);
void delete(String key);
void delete(String key);
}

View file

@ -0,0 +1,82 @@
package creator.loki.feature.counter;
import java.awt.Color;
import creator.loki.embed.EmbedFactory;
public class CounterConfigFormatter {
public static void sendChannelSetConfirmation(
net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel channel,
int memberCount,
net.dv8tion.jda.api.entities.channel.middleman.MessageChannel textChannel) {
textChannel
.sendMessageEmbeds(
EmbedFactory.create()
.withTitle("✅ Канал счетчика установлен")
.withDescription(
"Канал " + channel.getAsMention() + " теперь отображает количество участников")
.addField("Текущее количество", String.valueOf(memberCount), false)
.withColor(Color.GREEN)
.build())
.queue();
}
public static void sendFormatUpdated(
String format, net.dv8tion.jda.api.entities.channel.middleman.MessageChannel channel) {
channel
.sendMessageEmbeds(
EmbedFactory.create()
.withTitle("✅ Формат обновлен")
.withDescription("Новый формат: `" + format + "`")
.addField("Пример", format.replace("{count}", "123"), false)
.withColor(Color.GREEN)
.build())
.queue();
}
public static void sendInfo(
net.dv8tion.jda.api.entities.Guild guild,
String channelId,
String format,
net.dv8tion.jda.api.entities.channel.middleman.MessageChannel channel) {
var embed =
EmbedFactory.create()
.withTitle("📊 Конфигурация счетчика участников")
.withColor(Color.BLUE);
if (channelId != null) {
embed.addField("Канал", "<#" + channelId + ">", false);
} else {
embed.addField("Канал", "❌ Не установлен", false);
}
if (format != null) {
embed.addField("Формат", "`" + format + "`", false);
embed.addField(
"Пример", format.replace("{count}", String.valueOf(guild.getMemberCount())), false);
} else {
embed.addField("Формат", "❌ Не установлен", false);
}
channel.sendMessageEmbeds(embed.build()).queue();
}
public static void sendHelp(
net.dv8tion.jda.api.entities.channel.middleman.MessageChannel channel) {
channel
.sendMessageEmbeds(
EmbedFactory.create()
.withTitle("📊 Настройка счетчика участников")
.withDescription(
"""
**Доступные команды:**
• `!counter-setup channel #войс` или `id` - канал счётчика
• `!counter-setup format 【👥】Участники: {count}` - Установить формат
• `!counter-setup info` - Показать текущую конфигурацию
В формате используйте `{count}` для отображения количества.
Только для администраторов!
""")
.withColor(Color.BLUE)
.build())
.queue();
}
}

View file

@ -1,115 +1,102 @@
package creator.loki.feature.counter;
import java.util.*;
import creator.loki.feature.config.repository.ConfigRepository;
import creator.loki.feature.counter.repository.CounterStatsRepository;
import creator.loki.util.LoggerDecorator;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
/**
* Менеджер счетчика участников
*/
/** Менеджер счетчика участников */
public class CounterManager {
private static final Logger logger = LoggerFactory.getLogger(CounterManager.class);
private final CounterStatsRepository statsRepo;
private final ConfigRepository configRepo;
private static final Logger logger = LoggerFactory.getLogger(CounterManager.class);
private final CounterStatsRepository statsRepo;
private final ConfigRepository configRepo;
private String counterChannelId;
private String format = "【👥】Участники: {count}";
private String counterChannelId;
private String format = "【👥】Участники: {count}";
public CounterManager(CounterStatsRepository statsRepo, ConfigRepository configRepo) {
this.statsRepo = statsRepo;
this.configRepo = configRepo;
loadConfig();
public CounterManager(CounterStatsRepository statsRepo, ConfigRepository configRepo) {
this.statsRepo = statsRepo;
this.configRepo = configRepo;
loadConfig();
}
/** Загрузить конфигурацию */
private void loadConfig() {
this.counterChannelId = configRepo.get("counter.channel_id");
String savedFormat = configRepo.get("counter.format");
if (savedFormat != null) {
this.format = savedFormat;
}
}
/** Получить текущую дневную статистику */
public DailyStats getDailyStats() {
return statsRepo.getTodayStats();
}
/** Сохранить статистику */
private void saveDailyStats(DailyStats stats) {
statsRepo.saveStats(stats);
}
/** Зарегистрировать присоединение участника */
public void recordJoin() {
DailyStats stats = getDailyStats();
stats.incrementJoined();
saveDailyStats(stats);
logger.debug("Зарегистрировано присоединение. Всего за день: {}", stats.getJoined());
}
/** Зарегистрировать выход участника */
public void recordLeave() {
DailyStats stats = getDailyStats();
stats.incrementLeft();
saveDailyStats(stats);
logger.debug("Зарегистрирован выход. Всего за день: {}", stats.getLeft());
}
/** Обновить счетчик участников */
public void updateCounter(Guild guild) {
if (counterChannelId == null || counterChannelId.isEmpty()) {
return;
}
/**
* Загрузить конфигурацию
*/
private void loadConfig() {
this.counterChannelId = configRepo.get("counter.channel_id");
String savedFormat = configRepo.get("counter.format");
if (savedFormat != null) {
this.format = savedFormat;
}
VoiceChannel channel = guild.getVoiceChannelById(counterChannelId);
if (channel == null) {
LoggerDecorator.warn(logger, "Канал счетчика не найден: " + counterChannelId);
return;
}
/**
* Получить текущую дневную статистику
*/
public DailyStats getDailyStats() {
return statsRepo.getTodayStats();
int memberCount = guild.getMemberCount();
String newName = format.replace("{count}", String.valueOf(memberCount));
// Обновляем название канала только если оно изменилось
if (!channel.getName().equals(newName)) {
channel
.getManager()
.setName(newName)
.queue(
success -> logger.debug("Обновлен счетчик участников: {}", memberCount),
error -> LoggerDecorator.error(logger, "Ошибка при обновлении счетчика", error));
}
}
/**
* Сохранить статистику
*/
private void saveDailyStats(DailyStats stats) {
statsRepo.saveStats(stats);
}
/** Получить историю статистики */
public List<DailyStats> getHistory(int days) {
return statsRepo.getHistory(days);
}
/**
* Зарегистрировать присоединение участника
*/
public void recordJoin() {
DailyStats stats = getDailyStats();
stats.incrementJoined();
saveDailyStats(stats);
logger.debug("Зарегистрировано присоединение. Всего за день: {}", stats.getJoined());
}
// Getters
public String getCounterChannelId() {
return counterChannelId;
}
/**
* Зарегистрировать выход участника
*/
public void recordLeave() {
DailyStats stats = getDailyStats();
stats.incrementLeft();
saveDailyStats(stats);
logger.debug("Зарегистрирован выход. Всего за день: {}", stats.getLeft());
}
/**
* Обновить счетчик участников
*/
public void updateCounter(Guild guild) {
if (counterChannelId == null || counterChannelId.isEmpty()) {
return;
}
VoiceChannel channel = guild.getVoiceChannelById(counterChannelId);
if (channel == null) {
logger.warn("Канал счетчика не найден: {}", counterChannelId);
return;
}
int memberCount = guild.getMemberCount();
String newName = format.replace("{count}", String.valueOf(memberCount));
// Обновляем название канала только если оно изменилось
if (!channel.getName().equals(newName)) {
channel.getManager().setName(newName).queue(
success -> logger.debug("Обновлен счетчик участников: {}", memberCount),
error -> logger.error("Ошибка при обновлении счетчика", error)
);
}
}
/**
* Получить историю статистики
*/
public List<DailyStats> getHistory(int days) {
return statsRepo.getHistory(days);
}
// Getters
public String getCounterChannelId() {
return counterChannelId;
}
public String getFormat() {
return format;
}
public String getFormat() {
return format;
}
}

View file

@ -9,19 +9,19 @@ import creator.loki.feature.counter.repository.CounterStatsRepository;
public class CounterModule extends AbstractModule {
@Override
protected void configure() {
}
@Override
protected void configure() {}
@Provides
@Singleton
CounterStatsRepository provideCounterStatsRepository(Database database) {
return new CounterStatsRepository(database);
}
@Provides
@Singleton
CounterStatsRepository provideCounterStatsRepository(Database database) {
return new CounterStatsRepository(database);
}
@Provides
@Singleton
CounterManager provideCounterManager(CounterStatsRepository statsRepo, ConfigRepository configRepo) {
return new CounterManager(statsRepo, configRepo);
}
@Provides
@Singleton
CounterManager provideCounterManager(
CounterStatsRepository statsRepo, ConfigRepository configRepo) {
return new CounterManager(statsRepo, configRepo);
}
}

View file

@ -1,186 +1,136 @@
package creator.loki.feature.counter;
import java.util.List;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.feature.config.repository.ConfigRepository;
import creator.loki.embed.EmbedFactory;
import creator.loki.util.GuildChannelResolve;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
import java.awt.Color;
import java.util.List;
/**
* Prefix команда настройки счетчика участников
*/
public class CounterSetupPrefixCommand implements Command {
private final CounterManager counterManager;
private final ConfigRepository configRepo;
private final CounterManager counterManager;
private final ConfigRepository configRepo;
public CounterSetupPrefixCommand(CounterManager counterManager, ConfigRepository configRepo) {
this.counterManager = counterManager;
this.configRepo = configRepo;
public CounterSetupPrefixCommand(CounterManager counterManager, ConfigRepository configRepo) {
this.counterManager = counterManager;
this.configRepo = configRepo;
}
@Override
public String getName() {
return "counter-setup";
}
@Override
public String getDescription() {
return "Настройка счетчика участников";
}
@Override
public String getUsage() {
return "counter-setup <channel|format|info> [значение]";
}
@Override
public String getCategory() {
return "Настройка";
}
@Override
public void execute(CommandContext context) {
if (!context.getAuthor().hasPermission(Permission.ADMINISTRATOR)) {
context.getChannel().sendMessage("❌ У вас нет прав администратора!").queue();
return;
}
@Override
public String getName() {
return "counter-setup";
List<String> args = context.getArgs();
if (args.isEmpty()) {
CounterConfigFormatter.sendHelp(context.getChannel());
return;
}
@Override
public String getDescription() {
return "Настройка счетчика участников";
switch (args.get(0).toLowerCase()) {
case "channel" -> handleChannel(context, args);
case "format" -> handleFormat(context, args);
case "info" -> handleInfo(context);
default -> CounterConfigFormatter.sendHelp(context.getChannel());
}
}
private void handleChannel(CommandContext context, List<String> args) {
if (args.size() < 2) {
context
.getChannel()
.sendMessage(
"❌ Укажите голосовой канал: `#канал`, `<#id>` или числовой id."
+ "\nПример: `!counter-setup channel 123456789012345678`")
.queue();
return;
}
@Override
public String getUsage() {
return "counter-setup <channel|format|info> [значение]";
VoiceChannel channel =
GuildChannelResolve.voice(context.getGuild(), context.getMessage(), args, 1);
if (channel == null) {
context
.getChannel()
.sendMessage(
"❌ Не найден голосовой канал. Укажите `#войс`, `<#id>` или id из 17-20 цифр.")
.queue();
return;
}
@Override
public String getCategory() {
return "Настройка";
configRepo.set("counter.channel_id", channel.getId());
int memberCount = context.getGuild().getMemberCount();
String format = configRepo.get("counter.format");
if (format == null) {
format = "【👥】Участники: {count}";
configRepo.set("counter.format", format);
}
@Override
public void execute(CommandContext context) {
// Проверка админ прав
if (!context.getAuthor().hasPermission(Permission.ADMINISTRATOR)) {
context.getChannel().sendMessage("❌ У вас нет прав администратора!").queue();
return;
}
String newName = format.replace("{count}", String.valueOf(memberCount));
channel.getManager().setName(newName).queue();
List<String> args = context.getArgs();
if (args.isEmpty()) {
sendHelp(context);
return;
}
CounterConfigFormatter.sendChannelSetConfirmation(channel, memberCount, context.getChannel());
}
String action = args.get(0).toLowerCase();
switch (action) {
case "channel" -> handleChannel(context, args);
case "format" -> handleFormat(context, args);
case "info" -> handleInfo(context);
default -> sendHelp(context);
}
private void handleFormat(CommandContext context, List<String> args) {
if (args.size() < 2) {
context
.getChannel()
.sendMessage("❌ Укажите формат! Пример: `!counter-setup format 【👥】Участники: {count}`")
.queue();
return;
}
private void handleChannel(CommandContext context, List<String> args) {
if (args.size() < 2) {
context.getChannel().sendMessage(
"❌ Укажите голосовой канал: `#канал`, `<#id>` или числовой id. Пример: `!counter-setup channel 123456789012345678`"
).queue();
return;
}
String format = String.join(" ", args.subList(1, args.size()));
if (!format.contains("{count}")) {
context.getChannel().sendMessage("❌ Формат должен содержать `{count}`!").queue();
return;
}
VoiceChannel channel = GuildChannelResolve.voice(context.getGuild(), context.getMessage(), args, 1);
if (channel == null) {
context.getChannel().sendMessage(
"❌ Не найден голосовой канал. Укажите `#войс`, `<#id>` или id из 17-20 цифр."
).queue();
return;
}
configRepo.set("counter.channel_id", channel.getId());
configRepo.set("counter.format", format);
// Сразу обновляем название канала
String channelId = configRepo.get("counter.channel_id");
if (channelId != null) {
VoiceChannel channel = context.getGuild().getVoiceChannelById(channelId);
if (channel != null) {
int memberCount = context.getGuild().getMemberCount();
String format = configRepo.get("counter.format");
if (format == null) {
format = "【👥】Участники: {count}";
configRepo.set("counter.format", format);
}
String newName = format.replace("{count}", String.valueOf(memberCount));
channel.getManager().setName(newName).queue();
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.withTitle("✅ Канал счетчика установлен")
.withDescription("Канал " + channel.getAsMention() + " теперь отображает количество участников")
.addField("Текущее количество", String.valueOf(memberCount), false)
.withColor(Color.GREEN)
.build()
).queue();
channel
.getManager()
.setName(format.replace("{count}", String.valueOf(memberCount)))
.queue();
}
}
private void handleFormat(CommandContext context, List<String> args) {
if (args.size() < 2) {
context.getChannel().sendMessage("❌ Укажите формат! Пример: `!counter-setup format 【👥】Участники: {count}`").queue();
return;
}
CounterConfigFormatter.sendFormatUpdated(format, context.getChannel());
}
// Объединяем все аргументы в одну строку
String format = String.join(" ", args.subList(1, args.size()));
if (!format.contains("{count}")) {
context.getChannel().sendMessage("❌ Формат должен содержать `{count}`!").queue();
return;
}
configRepo.set("counter.format", format);
// Обновляем канал если он установлен
String channelId = configRepo.get("counter.channel_id");
if (channelId != null) {
VoiceChannel channel = context.getGuild().getVoiceChannelById(channelId);
if (channel != null) {
int memberCount = context.getGuild().getMemberCount();
String newName = format.replace("{count}", String.valueOf(memberCount));
channel.getManager().setName(newName).queue();
}
}
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.withTitle("✅ Формат обновлен")
.withDescription("Новый формат: `" + format + "`")
.addField("Пример", format.replace("{count}", "123"), false)
.withColor(Color.GREEN)
.build()
).queue();
}
private void handleInfo(CommandContext context) {
String channelId = configRepo.get("counter.channel_id");
String format = configRepo.get("counter.format");
var embed = EmbedFactory.create()
.withTitle("📊 Конфигурация счетчика участников")
.withColor(Color.BLUE);
if (channelId != null) {
embed.addField("Канал", "<#" + channelId + ">", false);
} else {
embed.addField("Канал", "❌ Не установлен", false);
}
if (format != null) {
embed.addField("Формат", "`" + format + "`", false);
embed.addField("Пример", format.replace("{count}", String.valueOf(context.getGuild().getMemberCount())), false);
} else {
embed.addField("Формат", "❌ Не установлен", false);
}
context.getChannel().sendMessageEmbeds(embed.build()).queue();
}
private void sendHelp(CommandContext context) {
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.withTitle("📊 Настройка счетчика участников")
.withDescription("""
**Доступные команды:**
• `!counter-setup channel #войс` или `id` - канал счётчика
• `!counter-setup format 【👥】Участники: {count}` - Установить формат
• `!counter-setup info` - Показать текущую конфигурацию
В формате используйте `{count}` для отображения количества.
Только для администраторов!
""")
.withColor(Color.BLUE)
.build()
).queue();
}
}
private void handleInfo(CommandContext context) {
String channelId = configRepo.get("counter.channel_id");
String format = configRepo.get("counter.format");
CounterConfigFormatter.sendInfo(context.getGuild(), channelId, format, context.getChannel());
}
}

View file

@ -2,78 +2,76 @@ package creator.loki.feature.counter;
import java.time.LocalDate;
/**
* Дневная статистика участников
*/
/** Дневная статистика участников */
public class DailyStats {
private String date;
private int joined;
private int left;
private int difference;
private String date;
private int joined;
private int left;
private int difference;
public DailyStats() {
this.date = LocalDate.now().toString();
this.joined = 0;
this.left = 0;
this.difference = 0;
}
public DailyStats() {
this.date = LocalDate.now().toString();
this.joined = 0;
this.left = 0;
this.difference = 0;
}
public DailyStats(String date, int joined, int left) {
this.date = date;
this.joined = joined;
this.left = left;
this.difference = joined - left;
}
public DailyStats(String date, int joined, int left) {
this.date = date;
this.joined = joined;
this.left = left;
this.difference = joined - left;
}
// Getters
public String getDate() {
return date;
}
// Getters
public String getDate() {
return date;
}
public int getJoined() {
return joined;
}
public int getJoined() {
return joined;
}
public int getLeft() {
return left;
}
public int getLeft() {
return left;
}
public int getDifference() {
return difference;
}
public int getDifference() {
return difference;
}
// Setters
public void setDate(String date) {
this.date = date;
}
public void setJoined(int joined) {
this.joined = joined;
updateDifference();
}
public void setLeft(int left) {
this.left = left;
updateDifference();
}
// Setters
public void setDate(String date) {
this.date = date;
}
// Методы
public void incrementJoined() {
this.joined++;
updateDifference();
}
public void setJoined(int joined) {
this.joined = joined;
updateDifference();
}
public void incrementLeft() {
this.left++;
updateDifference();
}
public void setLeft(int left) {
this.left = left;
updateDifference();
}
private void updateDifference() {
this.difference = this.joined - this.left;
}
// Методы
public void incrementJoined() {
this.joined++;
updateDifference();
}
/**
* Проверить является ли статистика сегодняшней
*/
public boolean isToday() {
return this.date.equals(LocalDate.now().toString());
}
public void incrementLeft() {
this.left++;
updateDifference();
}
private void updateDifference() {
this.difference = this.joined - this.left;
}
/** Проверить является ли статистика сегодняшней */
public boolean isToday() {
return this.date.equals(LocalDate.now().toString());
}
}

View file

@ -1,6 +1,11 @@
package creator.loki.feature.counter;
import creator.loki.feature.counter.CounterManager;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import com.google.inject.Inject;
import creator.loki.util.LoggerDecorator;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.events.guild.member.GuildMemberJoinEvent;
import net.dv8tion.jda.api.events.guild.member.GuildMemberRemoveEvent;
@ -8,81 +13,77 @@ import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.inject.Inject;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* Listener для счетчика участников
*/
/** Listener для счетчика участников */
public class MemberCounterListener extends ListenerAdapter {
private static final Logger logger = LoggerFactory.getLogger(MemberCounterListener.class);
private final CounterManager counterManager;
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
private Guild guild;
private static final Logger logger = LoggerFactory.getLogger(MemberCounterListener.class);
private final CounterManager counterManager;
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
private Guild guild;
@Inject
public MemberCounterListener(CounterManager counterManager) {
this.counterManager = counterManager;
@Inject
public MemberCounterListener(CounterManager counterManager) {
this.counterManager = counterManager;
}
/** Запустить периодическое обновление счетчика */
public void startScheduler(Guild guild) {
this.guild = guild;
// Обновляем счетчик каждые 5 минут
scheduler.scheduleAtFixedRate(
() ->
Thread.startVirtualThread(
() -> {
try {
counterManager.updateCounter(guild);
} catch (Exception e) {
LoggerDecorator.error(logger, "Ошибка при обновлении счетчика", e);
}
}),
0,
5,
TimeUnit.MINUTES);
LoggerDecorator.bot(logger, "Запущен планировщик обновления счетчика участников");
}
@Override
public void onGuildMemberJoin(GuildMemberJoinEvent event) {
// Игнорируем ботов
if (event.getUser().isBot()) {
return;
}
/**
* Запустить периодическое обновление счетчика
*/
public void startScheduler(Guild guild) {
this.guild = guild;
counterManager.recordJoin();
counterManager.updateCounter(event.getGuild());
// Обновляем счетчик каждые 5 минут
scheduler.scheduleAtFixedRate(() -> Thread.startVirtualThread(() -> {
try {
counterManager.updateCounter(guild);
} catch (Exception e) {
logger.error("Ошибка при обновлении счетчика", e);
}
}), 0, 5, TimeUnit.MINUTES);
LoggerDecorator.bot(
logger, "Участник " + event.getUser().getName() + " присоединился к серверу");
}
logger.info("Запущен планировщик обновления счетчика участников");
@Override
public void onGuildMemberRemove(GuildMemberRemoveEvent event) {
// Игнорируем ботов
if (event.getUser().isBot()) {
return;
}
@Override
public void onGuildMemberJoin(GuildMemberJoinEvent event) {
// Игнорируем ботов
if (event.getUser().isBot()) {
return;
}
counterManager.recordLeave();
counterManager.updateCounter(event.getGuild());
counterManager.recordJoin();
counterManager.updateCounter(event.getGuild());
LoggerDecorator.bot(logger, "Участник " + event.getUser().getName() + " покинул сервер");
}
logger.info("Участник {} присоединился к серверу", event.getUser().getName());
}
@Override
public void onGuildMemberRemove(GuildMemberRemoveEvent event) {
// Игнорируем ботов
if (event.getUser().isBot()) {
return;
}
counterManager.recordLeave();
counterManager.updateCounter(event.getGuild());
logger.info("Участник {} покинул сервер", event.getUser().getName());
}
/**
* Остановить планировщик
*/
public void shutdown() {
scheduler.shutdown();
try {
if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
scheduler.shutdownNow();
}
} catch (InterruptedException e) {
scheduler.shutdownNow();
Thread.currentThread().interrupt();
}
/** Остановить планировщик */
public void shutdown() {
scheduler.shutdown();
try {
if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
scheduler.shutdownNow();
}
} catch (InterruptedException e) {
scheduler.shutdownNow();
Thread.currentThread().interrupt();
}
}
}

View file

@ -1,10 +1,5 @@
package creator.loki.feature.counter.repository;
import creator.loki.database.Database;
import creator.loki.feature.counter.DailyStats;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
@ -12,62 +7,60 @@ import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
/**
* Репозиторий для работы со статистикой счетчика участников
*/
import creator.loki.database.Database;
import creator.loki.feature.counter.DailyStats;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** Репозиторий для работы со статистикой счетчика участников */
public class CounterStatsRepository {
private static final Logger logger = LoggerFactory.getLogger(CounterStatsRepository.class);
private final Database database;
private static final Logger logger = LoggerFactory.getLogger(CounterStatsRepository.class);
private final Database database;
public CounterStatsRepository(Database database) {
this.database = database;
}
public CounterStatsRepository(Database database) {
this.database = database;
}
/**
* Получить статистику за дату
*/
public DailyStats getStats(String date) {
String sql = "SELECT * FROM counter_stats WHERE date = ?";
/** Получить статистику за дату */
public DailyStats getStats(String date) {
String sql = "SELECT * FROM counter_stats WHERE date = ?";
try (PreparedStatement stmt = database.prepare(sql)) {
stmt.setString(1, date);
try (PreparedStatement stmt = database.prepare(sql)) {
stmt.setString(1, date);
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
DailyStats stats = new DailyStats();
stats.setDate(rs.getString("date"));
stats.setJoined(rs.getInt("joined"));
stats.setLeft(rs.getInt("left_count"));
return stats;
}
}
} catch (SQLException e) {
logger.error("Ошибка получения статистики за {}", date, e);
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
DailyStats stats = new DailyStats();
stats.setDate(rs.getString("date"));
stats.setJoined(rs.getInt("joined"));
stats.setLeft(rs.getInt("left_count"));
return stats;
}
return null;
}
} catch (SQLException e) {
logger.error("Ошибка получения статистики за {}", date, e);
}
/**
* Получить статистику за сегодня
*/
public DailyStats getTodayStats() {
String today = LocalDate.now().toString();
DailyStats stats = getStats(today);
return null;
}
if (stats == null) {
stats = new DailyStats();
stats.setDate(today);
}
/** Получить статистику за сегодня */
public DailyStats getTodayStats() {
String today = LocalDate.now().toString();
DailyStats stats = getStats(today);
return stats;
if (stats == null) {
stats = new DailyStats();
stats.setDate(today);
}
/**
* Сохранить статистику
*/
public void saveStats(DailyStats stats) {
String sql = """
return stats;
}
/** Сохранить статистику */
public void saveStats(DailyStats stats) {
String sql =
"""
INSERT INTO counter_stats (date, joined, left_count, difference)
VALUES (?, ?, ?, ?)
ON CONFLICT(date)
@ -77,61 +70,58 @@ public class CounterStatsRepository {
difference = excluded.difference
""";
try (PreparedStatement stmt = database.prepare(sql)) {
stmt.setString(1, stats.getDate());
stmt.setInt(2, stats.getJoined());
stmt.setInt(3, stats.getLeft());
stmt.setInt(4, stats.getDifference());
stmt.executeUpdate();
} catch (SQLException e) {
logger.error("Ошибка сохранения статистики за {}", stats.getDate(), e);
try (PreparedStatement stmt = database.prepare(sql)) {
stmt.setString(1, stats.getDate());
stmt.setInt(2, stats.getJoined());
stmt.setInt(3, stats.getLeft());
stmt.setInt(4, stats.getDifference());
stmt.executeUpdate();
} catch (SQLException e) {
logger.error("Ошибка сохранения статистики за {}", stats.getDate(), e);
}
}
/** Получить историю за последние N дней */
public List<DailyStats> getHistory(int days) {
List<DailyStats> history = new ArrayList<>();
String sql = "SELECT * FROM counter_stats ORDER BY date DESC LIMIT ?";
try (PreparedStatement stmt = database.prepare(sql)) {
stmt.setInt(1, days);
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
DailyStats stats = new DailyStats();
stats.setDate(rs.getString("date"));
stats.setJoined(rs.getInt("joined"));
stats.setLeft(rs.getInt("left_count"));
history.add(stats);
}
}
} catch (SQLException e) {
logger.error("Ошибка получения истории", e);
}
/**
* Получить историю за последние N дней
*/
public List<DailyStats> getHistory(int days) {
List<DailyStats> history = new ArrayList<>();
String sql = "SELECT * FROM counter_stats ORDER BY date DESC LIMIT ?";
java.util.Collections.reverse(history);
return history;
}
try (PreparedStatement stmt = database.prepare(sql)) {
stmt.setInt(1, days);
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
DailyStats stats = new DailyStats();
stats.setDate(rs.getString("date"));
stats.setJoined(rs.getInt("joined"));
stats.setLeft(rs.getInt("left_count"));
history.add(stats);
}
}
} catch (SQLException e) {
logger.error("Ошибка получения истории", e);
}
java.util.Collections.reverse(history);
return history;
}
/**
* Удалить старую статистику (старше N дней)
*/
public void cleanOldStats(int daysToKeep) {
String sql = """
/** Удалить старую статистику (старше N дней) */
public void cleanOldStats(int daysToKeep) {
String sql =
"""
DELETE FROM counter_stats
WHERE date < date('now', '-' || ? || ' days')
""";
try (PreparedStatement stmt = database.prepare(sql)) {
stmt.setInt(1, daysToKeep);
int deleted = stmt.executeUpdate();
if (deleted > 0) {
logger.info("Удалено {} старых записей статистики", deleted);
}
} catch (SQLException e) {
logger.error("Ошибка очистки старой статистики", e);
}
try (PreparedStatement stmt = database.prepare(sql)) {
stmt.setInt(1, daysToKeep);
int deleted = stmt.executeUpdate();
if (deleted > 0) {
logger.info("Удалено {} старых записей статистики", deleted);
}
} catch (SQLException e) {
logger.error("Ошибка очистки старой статистики", e);
}
}
}

View file

@ -22,42 +22,41 @@ import creator.loki.feature.economy.service.JobService;
import creator.loki.feature.economy.service.SlotService;
public class EconomyCommandRegistrar implements CommandRegistrar {
private final EconomyService economyService;
private final FishingService fishingService;
private final BlackjackService blackjackService;
private final SlotService slotService;
private final JobService jobService;
private final CooldownManager cooldownManager;
private final EconomyService economyService;
private final FishingService fishingService;
private final BlackjackService blackjackService;
private final SlotService slotService;
private final JobService jobService;
private final CooldownManager cooldownManager;
@Inject
public EconomyCommandRegistrar(
EconomyService economyService,
FishingService fishingService,
BlackjackService blackjackService,
SlotService slotService,
JobService jobService,
CooldownManager cooldownManager
) {
this.economyService = economyService;
this.fishingService = fishingService;
this.blackjackService = blackjackService;
this.slotService = slotService;
this.jobService = jobService;
this.cooldownManager = cooldownManager;
}
@Inject
public EconomyCommandRegistrar(
EconomyService economyService,
FishingService fishingService,
BlackjackService blackjackService,
SlotService slotService,
JobService jobService,
CooldownManager cooldownManager) {
this.economyService = economyService;
this.fishingService = fishingService;
this.blackjackService = blackjackService;
this.slotService = slotService;
this.jobService = jobService;
this.cooldownManager = cooldownManager;
}
@Override
public void register(CommandRegistry registry) {
registry.register(new BalanceCommand(economyService));
registry.register(new BaltopCommand(economyService));
registry.register(new PayCommand(economyService));
registry.register(new DailyCommand(economyService));
registry.register(new BankCommand(economyService));
registry.register(new RobCommand(economyService, cooldownManager));
registry.register(new FishCommand(fishingService));
registry.register(new BlackjackCommand(blackjackService, economyService));
registry.register(new SlotCommand(slotService, economyService));
registry.register(new JobCommand(jobService));
registry.register(new WorkCommand(jobService));
}
@Override
public void register(CommandRegistry registry) {
registry.register(new BalanceCommand(economyService));
registry.register(new BaltopCommand(economyService));
registry.register(new PayCommand(economyService));
registry.register(new DailyCommand(economyService));
registry.register(new BankCommand(economyService));
registry.register(new RobCommand(economyService, cooldownManager));
registry.register(new FishCommand(fishingService));
registry.register(new BlackjackCommand(blackjackService, economyService));
registry.register(new SlotCommand(slotService, economyService));
registry.register(new JobCommand(jobService));
registry.register(new WorkCommand(jobService));
}
}

View file

@ -15,43 +15,49 @@ import creator.loki.feature.economy.service.SlotService;
public class EconomyModule extends AbstractModule {
@Override
protected void configure() {
}
@Override
protected void configure() {}
@Provides
@Singleton
EconomyRepository provideEconomyRepository(Database database) {
return new SqlEconomyRepository(database);
}
@Provides
@Singleton
EconomyRepository provideEconomyRepository(Database database) {
return new SqlEconomyRepository(database);
}
@Provides
@Singleton
EconomyService provideEconomyService(EconomyRepository repository, CooldownManager cooldownManager) {
return new EconomyService(repository, cooldownManager);
}
@Provides
@Singleton
EconomyService provideEconomyService(
EconomyRepository repository, CooldownManager cooldownManager) {
return new EconomyService(repository, cooldownManager);
}
@Provides
@Singleton
FishingService provideFishingService(EconomyRepository repository) {
return new FishingService(repository);
}
@Provides
@Singleton
FishingService provideFishingService(EconomyRepository repository) {
return new FishingService(repository);
}
@Provides
@Singleton
BlackjackService provideBlackjackService(EconomyRepository repository) {
return new BlackjackService(repository);
}
@Provides
@Singleton
BlackjackService provideBlackjackService(EconomyRepository repository) {
return new BlackjackService(repository);
}
@Provides
@Singleton
SlotService provideSlotService(EconomyRepository repository) {
return new SlotService(repository);
}
@Provides
@Singleton
SlotService provideSlotService(EconomyRepository repository) {
return new SlotService(repository);
}
@Provides
@Singleton
JobService provideJobService(EconomyRepository repository) {
return new JobService(repository);
}
@Provides
@Singleton
JobService provideJobService(EconomyRepository repository) {
return new JobService(repository);
}
@Provides
@Singleton
FishMessageListener provideFishMessageListener(FishingService fishingService) {
return new FishMessageListener(fishingService);
}
}

View file

@ -0,0 +1,78 @@
package creator.loki.feature.economy;
import java.awt.Color;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import creator.loki.embed.EmbedFactory;
import creator.loki.feature.economy.service.FishingService;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.jetbrains.annotations.NotNull;
@Singleton
public class FishMessageListener extends ListenerAdapter {
private final FishingService fishingService;
@Inject
public FishMessageListener(FishingService fishingService) {
this.fishingService = fishingService;
}
@Override
public void onMessageReceived(@NotNull MessageReceivedEvent event) {
if (event.getAuthor().isBot()) return;
String content = event.getMessage().getContentRaw().trim();
int answer;
try {
answer = Integer.parseInt(content);
} catch (NumberFormatException e) {
return;
}
String userId = event.getAuthor().getId();
FishingService.FishResult result = fishingService.checkAnswer(userId, answer);
if (result == null) return;
if (result.reward() > 0) {
event
.getChannel()
.sendMessageEmbeds(
EmbedFactory.create()
.withTitle("\uD83C\uDFA3 Улов!")
.withDescription(
"**"
+ event.getAuthor().getAsMention()
+ "**, ты поймал "
+ result.count()
+ " "
+ result.targetFish()
+ "!\n"
+ "Твой баланс: **"
+ result.cashAfter()
+ "** LC")
.withColor(new Color(0, 200, 0))
.build())
.queue();
} else {
event
.getChannel()
.sendMessageEmbeds(
EmbedFactory.create()
.withTitle("\uD83D\uDC1F Рыбалка — мимо!")
.withDescription(
"**"
+ event.getAuthor().getAsMention()
+ "**, правильный ответ: **"
+ result.count()
+ "** "
+ result.targetFish())
.withColor(new Color(200, 0, 0))
.build())
.queue();
}
}
}

View file

@ -1,101 +1,118 @@
package creator.loki.feature.economy.command.bank;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.feature.economy.domain.EconomyAccount;
import creator.loki.feature.economy.service.EconomyService;
import creator.loki.embed.EmbedFactory;
import creator.loki.embed.EmbedTemplates;
import java.awt.Color;
import java.util.List;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import creator.loki.embed.EmbedTemplates;
import creator.loki.feature.economy.domain.EconomyAccount;
import creator.loki.feature.economy.service.EconomyService;
public class BankCommand implements Command {
private final EconomyService economyService;
private final EconomyService economyService;
public BankCommand(EconomyService economyService) {
this.economyService = economyService;
}
public BankCommand(EconomyService economyService) {
this.economyService = economyService;
}
@Override
public String getName() {
return "bank";
}
@Override
public String getName() {
return "bank";
}
@Override
public String getDescription() {
return "Управление банковским счётом";
}
@Override
public String getDescription() {
return "Управление банковским счётом";
}
@Override
public String getUsage() {
return "bank [deposit|withdraw|balance] <amount>";
}
@Override
public String getUsage() {
return "bank [deposit|withdraw|balance] <amount>";
}
@Override
public String getCategory() {
return "Экономика";
}
@Override
public String getCategory() {
return "Экономика";
}
@Override
public void execute(CommandContext ctx) {
List<String> args = ctx.getArgs();
String uid = ctx.getAuthor().getId();
if (args.isEmpty() || args.get(0).equalsIgnoreCase("balance")) {
EconomyAccount acc = economyService.getAccount(uid);
ctx.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.withTitle("🏦 Банк — " + ctx.getAuthor().getEffectiveName())
.withColor(new Color(52, 152, 219))
.addField("Наличные", "**" + acc.getCash() + "** LC", true)
.addField("Банк", "**" + acc.getBank() + "** LC", true)
.addField("Всего", "**" + acc.getTotalBalance() + "** LC", true)
.build()
).queue();
return;
}
String sub = args.get(0).toLowerCase();
if (!sub.equals("deposit") && !sub.equals("withdraw")) {
ctx.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Неизвестная подкоманда. Используй: " + getUsage())
).queue();
return;
}
if (args.size() < 2) {
ctx.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Укажите сумму: bank " + sub + " <amount>")
).queue();
return;
}
long amount;
try {
amount = Long.parseLong(args.get(1));
} catch (NumberFormatException e) {
ctx.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Сумма должна быть числом!")
).queue();
return;
}
if (amount <= 0) {
ctx.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Сумма должна быть положительной!")
).queue();
return;
}
EconomyAccount acc = economyService.getAccount(uid);
boolean isDep = sub.equals("deposit");
long balance = isDep ? acc.getCash() : acc.getBank();
if (balance < amount) {
ctx.getChannel().sendMessageEmbeds(EmbedTemplates.error("Ошибка",
"Недостаточно " + (isDep ? "наличных" : "в банке") + "! У вас **" + balance + "** LC")
).queue();
return;
}
if (isDep) economyService.deposit(uid, amount);
else economyService.withdraw(uid, amount);
ctx.getChannel().sendMessageEmbeds(
EmbedTemplates.success(isDep ? "Депозит" : "Снятие",
"Ты " + (isDep ? "положил" : "снял") + " **" + amount + "** LC " + (isDep ? "в банк!" : "из банка!"))
).queue();
@Override
public void execute(CommandContext ctx) {
List<String> args = ctx.getArgs();
String uid = ctx.getAuthor().getId();
if (args.isEmpty() || args.get(0).equalsIgnoreCase("balance")) {
EconomyAccount acc = economyService.getAccount(uid);
ctx.getChannel()
.sendMessageEmbeds(
EmbedFactory.create()
.withTitle("🏦 Банк — " + ctx.getAuthor().getEffectiveName())
.withColor(new Color(52, 152, 219))
.addField("Наличные", "**" + acc.getCash() + "** LC", true)
.addField("Банк", "**" + acc.getBank() + "** LC", true)
.addField("Всего", "**" + acc.getTotalBalance() + "** LC", true)
.build())
.queue();
return;
}
String sub = args.get(0).toLowerCase();
if (!sub.equals("deposit") && !sub.equals("withdraw")) {
ctx.getChannel()
.sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Неизвестная подкоманда. Используй: " + getUsage()))
.queue();
return;
}
if (args.size() < 2) {
ctx.getChannel()
.sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Укажите сумму: bank " + sub + " <amount>"))
.queue();
return;
}
long amount;
try {
amount = Long.parseLong(args.get(1));
} catch (NumberFormatException e) {
ctx.getChannel()
.sendMessageEmbeds(EmbedTemplates.error("Ошибка", "Сумма должна быть числом!"))
.queue();
return;
}
if (amount <= 0) {
ctx.getChannel()
.sendMessageEmbeds(EmbedTemplates.error("Ошибка", "Сумма должна быть положительной!"))
.queue();
return;
}
EconomyAccount acc = economyService.getAccount(uid);
boolean isDep = sub.equals("deposit");
long balance = isDep ? acc.getCash() : acc.getBank();
if (balance < amount) {
ctx.getChannel()
.sendMessageEmbeds(
EmbedTemplates.error(
"Ошибка",
"Недостаточно "
+ (isDep ? "наличных" : "в банке")
+ "! У вас **"
+ balance
+ "** LC"))
.queue();
return;
}
if (isDep) economyService.deposit(uid, amount);
else economyService.withdraw(uid, amount);
ctx.getChannel()
.sendMessageEmbeds(
EmbedTemplates.success(
isDep ? "Депозит" : "Снятие",
"Ты "
+ (isDep ? "положил" : "снял")
+ " **"
+ amount
+ "** LC "
+ (isDep ? "в банк!" : "из банка!")))
.queue();
}
}

View file

@ -1,57 +1,59 @@
package creator.loki.feature.economy.command.core;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.feature.economy.domain.EconomyAccount;
import creator.loki.feature.economy.service.EconomyService;
import creator.loki.embed.EmbedFactory;
import net.dv8tion.jda.api.entities.Member;
import java.awt.Color;
import java.util.List;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import creator.loki.feature.economy.domain.EconomyAccount;
import creator.loki.feature.economy.service.EconomyService;
import net.dv8tion.jda.api.entities.Member;
public class BalanceCommand implements Command {
private final EconomyService economyService;
private final EconomyService economyService;
public BalanceCommand(EconomyService economyService) {
this.economyService = economyService;
}
public BalanceCommand(EconomyService economyService) {
this.economyService = economyService;
}
@Override
public String getName() {
return "balance";
}
@Override
public String getName() {
return "balance";
}
@Override
public String getDescription() {
return "Показать баланс пользователя";
}
@Override
public String getDescription() {
return "Показать баланс пользователя";
}
@Override
public String getUsage() {
return "balance [@user]";
}
@Override
public String getUsage() {
return "balance [@user]";
}
@Override
public String getCategory() {
return "Экономика";
}
@Override
public String getCategory() {
return "Экономика";
}
@Override
public void execute(CommandContext context) {
List<Member> mentioned = context.getMessage().getMentions().getMembers();
Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0);
@Override
public void execute(CommandContext context) {
List<Member> mentioned = context.getMessage().getMentions().getMembers();
Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0);
EconomyAccount account = economyService.getAccount(target.getId());
EconomyAccount account = economyService.getAccount(target.getId());
context.getChannel().sendMessageEmbeds(
context
.getChannel()
.sendMessageEmbeds(
EmbedFactory.create()
.withTitle("💰 Баланс — " + target.getEffectiveName())
.withColor(new Color(255, 215, 0))
.addField("Наличные", "**" + account.getCash() + "** LC", true)
.addField("Банк", "**" + account.getBank() + "** LC", true)
.addField("Всего", "**" + account.getTotalBalance() + "** LC", true)
.build()
).queue();
}
.build())
.queue();
}
}

View file

@ -1,57 +1,64 @@
package creator.loki.feature.economy.command.core;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.feature.economy.domain.EconomyAccount;
import creator.loki.feature.economy.service.EconomyService;
import creator.loki.embed.EmbedFactory;
import java.awt.Color;
import java.util.List;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import creator.loki.feature.economy.domain.EconomyAccount;
import creator.loki.feature.economy.service.EconomyService;
public class BaltopCommand implements Command {
private final EconomyService economyService;
private final EconomyService economyService;
public BaltopCommand(EconomyService economyService) {
this.economyService = economyService;
public BaltopCommand(EconomyService economyService) {
this.economyService = economyService;
}
@Override
public String getName() {
return "baltop";
}
@Override
public String getDescription() {
return "Топ 10 богатейших пользователей";
}
@Override
public String getCategory() {
return "Экономика";
}
@Override
public void execute(CommandContext context) {
List<EconomyAccount> top = economyService.getTopBalances(10);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < top.size(); i++) {
EconomyAccount acc = top.get(i);
String name =
context.getGuild().getMemberById(acc.getUserId()) != null
? context.getGuild().getMemberById(acc.getUserId()).getEffectiveName()
: acc.getUserId();
sb.append("**")
.append(i + 1)
.append(".** ")
.append(name)
.append(" — **")
.append(acc.getTotalBalance())
.append("** LC\n");
}
@Override
public String getName() {
return "baltop";
}
@Override
public String getDescription() {
return "Топ 10 богатейших пользователей";
}
@Override
public String getCategory() {
return "Экономика";
}
@Override
public void execute(CommandContext context) {
List<EconomyAccount> top = economyService.getTopBalances(10);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < top.size(); i++) {
EconomyAccount acc = top.get(i);
String name = context.getGuild().getMemberById(acc.getUserId()) != null
? context.getGuild().getMemberById(acc.getUserId()).getEffectiveName()
: acc.getUserId();
sb.append("**").append(i + 1).append(".** ")
.append(name)
.append(" — **").append(acc.getTotalBalance()).append("** LC\n");
}
context.getChannel().sendMessageEmbeds(
context
.getChannel()
.sendMessageEmbeds(
EmbedFactory.create()
.withTitle("🏆 Топ богатейших")
.withDescription(sb.toString())
.withColor(new Color(255, 215, 0))
.build()
).queue();
}
.build())
.queue();
}
}

View file

@ -1,55 +1,59 @@
package creator.loki.feature.economy.command.core;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.feature.economy.service.EconomyService;
import creator.loki.embed.EmbedFactory;
import creator.loki.embed.EmbedTemplates;
import java.awt.Color;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import creator.loki.embed.EmbedTemplates;
import creator.loki.feature.economy.service.EconomyService;
public class DailyCommand implements Command {
private final EconomyService economyService;
private final EconomyService economyService;
public DailyCommand(EconomyService economyService) {
this.economyService = economyService;
public DailyCommand(EconomyService economyService) {
this.economyService = economyService;
}
@Override
public String getName() {
return "daily";
}
@Override
public String getDescription() {
return "Получить ежедневную награду";
}
@Override
public String getCategory() {
return "Экономика";
}
@Override
public void execute(CommandContext context) {
EconomyService.DailyResult result = economyService.claimDaily(context.getAuthor().getId());
if (result == null) {
context
.getChannel()
.sendMessageEmbeds(
EmbedTemplates.warning(
"Ежедневная награда", "Ты уже получил награду сегодня! Возвращайся завтра."))
.queue();
return;
}
@Override
public String getName() {
return "daily";
}
@Override
public String getDescription() {
return "Получить ежедневную награду";
}
@Override
public String getCategory() {
return "Экономика";
}
@Override
public void execute(CommandContext context) {
EconomyService.DailyResult result = economyService.claimDaily(context.getAuthor().getId());
if (result == null) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.warning("Ежедневная награда",
"Ты уже получил награду сегодня! Возвращайся завтра.")
).queue();
return;
}
context.getChannel().sendMessageEmbeds(
context
.getChannel()
.sendMessageEmbeds(
EmbedFactory.create()
.withTitle("🎁 Ежедневная награда")
.withDescription("Ты получил **" + result.amount() + "** LC!")
.withColor(new Color(46, 204, 113))
.addField("Стрик", "**" + result.streak() + "** дней", true)
.addField("Баланс", "**" + result.newCash() + "** LC", true)
.build()
).queue();
}
.build())
.queue();
}
}

View file

@ -1,96 +1,112 @@
package creator.loki.feature.economy.command.core;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.feature.economy.service.EconomyService;
import creator.loki.embed.EmbedTemplates;
import net.dv8tion.jda.api.entities.Member;
import java.util.List;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedTemplates;
import creator.loki.feature.economy.service.EconomyService;
import net.dv8tion.jda.api.entities.Member;
public class PayCommand implements Command {
private final EconomyService economyService;
private final EconomyService economyService;
public PayCommand(EconomyService economyService) {
this.economyService = economyService;
public PayCommand(EconomyService economyService) {
this.economyService = economyService;
}
@Override
public String getName() {
return "pay";
}
@Override
public String getDescription() {
return "Перевести деньги другому пользователю";
}
@Override
public String getUsage() {
return "pay <@user> <amount>";
}
@Override
public String getCategory() {
return "Экономика";
}
@Override
public void execute(CommandContext context) {
List<Member> mentioned = context.getMessage().getMentions().getMembers();
if (mentioned.isEmpty()) {
context
.getChannel()
.sendMessageEmbeds(EmbedTemplates.error("Ошибка", "Укажите пользователя: " + getUsage()))
.queue();
return;
}
@Override
public String getName() {
return "pay";
Member target = mentioned.get(0);
if (target.getId().equals(context.getAuthor().getId())) {
context
.getChannel()
.sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Нельзя переводить деньги самому себе!"))
.queue();
return;
}
@Override
public String getDescription() {
return "Перевести деньги другому пользователю";
List<String> args = context.getArgs();
if (args.size() < 2) {
context
.getChannel()
.sendMessageEmbeds(EmbedTemplates.error("Ошибка", "Укажите сумму: " + getUsage()))
.queue();
return;
}
@Override
public String getUsage() {
return "pay <@user> <amount>";
long amount;
try {
amount = Long.parseLong(args.get(1));
} catch (NumberFormatException e) {
context
.getChannel()
.sendMessageEmbeds(EmbedTemplates.error("Ошибка", "Сумма должна быть числом!"))
.queue();
return;
}
@Override
public String getCategory() {
return "Экономика";
if (amount <= 0) {
context
.getChannel()
.sendMessageEmbeds(EmbedTemplates.error("Ошибка", "Сумма должна быть положительной!"))
.queue();
return;
}
@Override
public void execute(CommandContext context) {
List<Member> mentioned = context.getMessage().getMentions().getMembers();
if (mentioned.isEmpty()) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Укажите пользователя: " + getUsage())
).queue();
return;
}
Member target = mentioned.get(0);
if (target.getId().equals(context.getAuthor().getId())) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Нельзя переводить деньги самому себе!")
).queue();
return;
}
List<String> args = context.getArgs();
if (args.size() < 2) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Укажите сумму: " + getUsage())
).queue();
return;
}
long amount;
try {
amount = Long.parseLong(args.get(1));
} catch (NumberFormatException e) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Сумма должна быть числом!")
).queue();
return;
}
if (amount <= 0) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Сумма должна быть положительной!")
).queue();
return;
}
var senderAccount = economyService.getAccount(context.getAuthor().getId());
if (senderAccount.getCash() < amount) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Недостаточно средств! У вас **" + senderAccount.getCash() + "** LC")
).queue();
return;
}
economyService.transfer(context.getAuthor().getId(), target.getId(), amount);
context.getChannel().sendMessageEmbeds(
EmbedTemplates.success("Перевод выполнен",
context.getAuthor().getAsMention() + " перевёл **" + amount + "** LC " + target.getAsMention())
).queue();
var senderAccount = economyService.getAccount(context.getAuthor().getId());
if (senderAccount.getCash() < amount) {
context
.getChannel()
.sendMessageEmbeds(
EmbedTemplates.error(
"Ошибка", "Недостаточно средств! У вас **" + senderAccount.getCash() + "** LC"))
.queue();
return;
}
economyService.transfer(context.getAuthor().getId(), target.getId(), amount);
context
.getChannel()
.sendMessageEmbeds(
EmbedTemplates.success(
"Перевод выполнен",
context.getAuthor().getAsMention()
+ " перевёл **"
+ amount
+ "** LC "
+ target.getAsMention()))
.queue();
}
}

View file

@ -1,148 +1,162 @@
package creator.loki.feature.economy.command.minigame;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.feature.economy.service.BlackjackService;
import creator.loki.feature.economy.service.EconomyService;
import creator.loki.embed.EmbedFactory;
import creator.loki.embed.EmbedTemplates;
import net.dv8tion.jda.api.entities.MessageEmbed;
import java.awt.Color;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import creator.loki.embed.EmbedTemplates;
import creator.loki.feature.economy.service.BlackjackService;
import creator.loki.feature.economy.service.EconomyService;
import net.dv8tion.jda.api.entities.MessageEmbed;
public class BlackjackCommand implements Command {
private final BlackjackService blackjackService;
private final EconomyService economyService;
private final Set<String> activeGames = ConcurrentHashMap.newKeySet();
private final BlackjackService blackjackService;
private final EconomyService economyService;
private final Set<String> activeGames = ConcurrentHashMap.newKeySet();
public BlackjackCommand(BlackjackService blackjackService, EconomyService economyService) {
this.blackjackService = blackjackService;
this.economyService = economyService;
public BlackjackCommand(BlackjackService blackjackService, EconomyService economyService) {
this.blackjackService = blackjackService;
this.economyService = economyService;
}
@Override
public String getName() {
return "blackjack";
}
@Override
public String getDescription() {
return "Блэкджек — играй против дилера";
}
@Override
public String getUsage() {
return "bj <ставка> — начать игру\nbj hit — взять карту\nbj stand — остановиться";
}
@Override
public String getCategory() {
return "Экономика";
}
@Override
public void execute(CommandContext context) {
var args = context.getArgs();
var userId = context.getAuthor().getId();
if (args.isEmpty()) {
context
.getChannel()
.sendMessageEmbeds(
EmbedTemplates.warning("Блэкджек", "Используй: `!bj <ставка>` чтобы начать"))
.queue();
return;
}
@Override
public String getName() {
return "blackjack";
var action = args.get(0).toLowerCase();
if (action.equals("hit")) {
handleAction(context, userId, true);
} else if (action.equals("stand")) {
handleAction(context, userId, false);
} else {
handleStart(context, userId, action);
}
}
@Override
public String getDescription() {
return "Блэкджек — играй против дилера";
private void handleStart(CommandContext context, String userId, String betStr) {
if (activeGames.contains(userId)) {
context
.getChannel()
.sendMessageEmbeds(
EmbedTemplates.error(
"Ошибка", "У тебя уже есть активная игра! Используй `!bj hit` или `!bj stand`"))
.queue();
return;
}
@Override
public String getUsage() {
return "bj <ставка> — начать игру\nbj hit — взять карту\nbj stand — остановиться";
long bet;
try {
bet = Long.parseLong(betStr);
if (bet <= 0) throw new NumberFormatException();
} catch (NumberFormatException e) {
context
.getChannel()
.sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Укажи положительное число в качестве ставки"))
.queue();
return;
}
@Override
public String getCategory() {
return "Экономика";
var account = economyService.getAccount(userId);
if (account.getCash() < bet) {
context
.getChannel()
.sendMessageEmbeds(
EmbedTemplates.error(
"Ошибка", "Недостаточно средств! Твой баланс: " + account.getCash() + " LC"))
.queue();
return;
}
@Override
public void execute(CommandContext context) {
var args = context.getArgs();
var userId = context.getAuthor().getId();
if (args.isEmpty()) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.warning("Блэкджек", "Используй: `!bj <ставка>` чтобы начать")
).queue();
return;
}
var action = args.get(0).toLowerCase();
if (action.equals("hit")) {
handleAction(context, userId, true);
} else if (action.equals("stand")) {
handleAction(context, userId, false);
} else {
handleStart(context, userId, action);
}
BlackjackService.BlackjackState state = blackjackService.startGame(userId, bet);
if (state == null) {
context
.getChannel()
.sendMessageEmbeds(EmbedTemplates.error("Ошибка", "Не удалось начать игру."))
.queue();
return;
}
if (!state.isOver()) activeGames.add(userId);
context.getChannel().sendMessageEmbeds(buildGameEmbed(state)).queue();
}
private void handleStart(CommandContext context, String userId, String betStr) {
if (activeGames.contains(userId)) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "У тебя уже есть активная игра! Используй `!bj hit` или `!bj stand`")
).queue();
return;
}
long bet;
try {
bet = Long.parseLong(betStr);
if (bet <= 0) throw new NumberFormatException();
} catch (NumberFormatException e) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Укажи положительное число в качестве ставки")
).queue();
return;
}
var account = economyService.getAccount(userId);
if (account.getCash() < bet) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Недостаточно средств! Твой баланс: " + account.getCash() + " LC")
).queue();
return;
}
BlackjackService.BlackjackState state = blackjackService.startGame(userId, bet);
if (state == null) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Не удалось начать игру.")
).queue();
return;
}
if (!state.isOver()) activeGames.add(userId);
context.getChannel().sendMessageEmbeds(buildGameEmbed(state)).queue();
private void handleAction(CommandContext context, String userId, boolean isHit) {
if (!activeGames.contains(userId)) {
context
.getChannel()
.sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Нет активной игры! Начни с `!bj <ставка>`"))
.queue();
return;
}
BlackjackService.BlackjackState state =
isHit ? blackjackService.hit(userId) : blackjackService.stand(userId);
if (state.isOver()) activeGames.remove(userId);
context.getChannel().sendMessageEmbeds(buildGameEmbed(state)).queue();
}
private void handleAction(CommandContext context, String userId, boolean isHit) {
if (!activeGames.contains(userId)) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Нет активной игры! Начни с `!bj <ставка>`")
).queue();
return;
}
BlackjackService.BlackjackState state = isHit
? blackjackService.hit(userId)
: blackjackService.stand(userId);
if (state.isOver()) activeGames.remove(userId);
context.getChannel().sendMessageEmbeds(buildGameEmbed(state)).queue();
}
private MessageEmbed buildGameEmbed(BlackjackService.BlackjackState state) {
var embed = EmbedFactory.create()
private MessageEmbed buildGameEmbed(BlackjackService.BlackjackState state) {
var embed =
EmbedFactory.create()
.withTitle("🃏 Блэкджек")
.withColor(new Color(34, 139, 34))
.addField("Твои карты", formatCards(state.playerCards(), state.playerSum()), true);
if (state.isOver()) {
embed.addField("Карты дилера", formatCards(state.dealerCards(), state.dealerSum()), true)
.addField("Результат", formatResult(state), false);
} else {
embed.addField("Карты дилера", formatHiddenDealerCards(state.dealerCards()), true)
.withFooter("!bj hit — взять карту | !bj stand — остановиться");
}
return embed.addField("Ставка", state.bet() + " LC", false).build();
if (state.isOver()) {
embed
.addField("Карты дилера", formatCards(state.dealerCards(), state.dealerSum()), true)
.addField("Результат", formatResult(state), false);
} else {
embed
.addField("Карты дилера", formatHiddenDealerCards(state.dealerCards()), true)
.withFooter("!bj hit — взять карту | !bj stand — остановиться");
}
return embed.addField("Ставка", state.bet() + " LC", false).build();
}
private String formatCards(String[] cards, int sum) {
return String.join(" + ", cards) + " = " + sum;
}
private String formatCards(String[] cards, int sum) {
return String.join(" + ", cards) + " = " + sum;
}
private String formatHiddenDealerCards(String[] cards) {
var sb = new StringBuilder("?");
for (int i = 1; i < cards.length; i++) sb.append(" + ").append(cards[i]);
return sb.toString();
}
private String formatHiddenDealerCards(String[] cards) {
var sb = new StringBuilder("?");
for (int i = 1; i < cards.length; i++) sb.append(" + ").append(cards[i]);
return sb.toString();
}
private String formatResult(BlackjackService.BlackjackState state) {
long payout = state.payout();
if (payout > state.bet()) return "🎉 Блэкджек! +" + payout + " LC";
if (payout > 0) return "✅ Ты выиграл +" + payout + " LC";
if (payout == state.bet()) return "🤝 Ничья. Возврат " + payout + " LC";
return "💔 Ты проиграл " + state.bet() + " LC";
}
private String formatResult(BlackjackService.BlackjackState state) {
long payout = state.payout();
if (payout > state.bet()) return "🎉 Блэкджек! +" + payout + " LC";
if (payout > 0) return "✅ Ты выиграл +" + payout + " LC";
if (payout == state.bet()) return "🤝 Ничья. Возврат " + payout + " LC";
return "💔 Ты проиграл " + state.bet() + " LC";
}
}

View file

@ -1,106 +1,73 @@
package creator.loki.feature.economy.command.minigame;
import java.awt.Color;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.feature.economy.service.FishingService;
import creator.loki.embed.EmbedFactory;
import creator.loki.embed.EmbedTemplates;
import java.awt.Color;
import java.util.concurrent.ConcurrentHashMap;
import creator.loki.feature.economy.service.FishingService;
public class FishCommand implements Command {
private static record FishSession(String difficulty, long startTime) {}
private final FishingService fishingService;
private final FishingService fishingService;
private final ConcurrentHashMap<String, FishSession> sessions = new ConcurrentHashMap<>();
public FishCommand(FishingService fishingService) {
this.fishingService = fishingService;
}
public FishCommand(FishingService fishingService) {
this.fishingService = fishingService;
@Override
public String getName() {
return "fish";
}
@Override
public String getDescription() {
return "Начать рыбалку — угадай количество рыбы в пруду";
}
@Override
public String getUsage() {
return "fish [easy/normal/hard] — начать рыбалку\nПотом просто напиши число в чат";
}
@Override
public String getCategory() {
return "Экономика";
}
@Override
public void execute(CommandContext context) {
var args = context.getArgs();
var userId = context.getAuthor().getId();
String difficulty = "easy";
if (!args.isEmpty()) {
String first = args.get(0).toLowerCase();
if (first.equals("easy") || first.equals("normal") || first.equals("hard")) {
difficulty = first;
}
}
@Override
public String getName() {
return "fish";
FishingService.FishResult result = fishingService.startFishing(userId, difficulty);
if (result == null) {
context
.getChannel()
.sendMessageEmbeds(EmbedTemplates.warning("Рыбалка", "Подожди 2 часа между рыбалками!"))
.queue();
return;
}
@Override
public String getDescription() {
return "Рыбалка — угадай количество рыбы в пруду";
}
@Override
public String getUsage() {
return "fish <easy/normal/hard> — начать рыбалку\nfish <число> — ответить";
}
@Override
public String getCategory() {
return "Экономика";
}
@Override
public void execute(CommandContext context) {
var args = context.getArgs();
var userId = context.getAuthor().getId();
if (args.isEmpty()) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.warning("Рыбалка", "Используй: `!fish easy/normal/hard`")
).queue();
return;
}
var first = args.get(0).toLowerCase();
if (first.equals("easy") || first.equals("normal") || first.equals("hard")) {
FishingService.FishResult result = fishingService.startFishing(userId, first);
sessions.put(userId, new FishSession(first, System.currentTimeMillis()));
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.withTitle("🎣 Рыбалка")
.withDescription("```\n" + String.join("\n", result.grid()) + "\n```")
.withColor(new Color(0, 150, 255))
.addField("Вопрос", "Сколько **" + result.targetFish() + "** ты видишь?", false)
.withFooter("Напиши !fish <число>, чтобы ответить")
.build()
).queue();
return;
}
try {
int answer = Integer.parseInt(first);
FishSession session = sessions.remove(userId);
if (session == null) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Нет активной рыбалки! Начни с `!fish easy/normal/hard`")
).queue();
return;
}
FishingService.FishResult result = fishingService.checkAnswer(userId, answer, session.startTime(), session.difficulty());
var embed = EmbedFactory.create()
.withTitle("🎣 Рыбалка — Результат")
context
.getChannel()
.sendMessageEmbeds(
EmbedFactory.create()
.withTitle("🎣 Рыбалка — " + difficulty)
.withDescription("```\n" + String.join("\n", result.grid()) + "\n```")
.withColor(new Color(255, 215, 0));
if (result.reward() > 0) {
embed.addField("Твой ответ", String.valueOf(answer), true)
.addField("Правильный ответ", String.valueOf(result.count()), true)
.addField("Награда", "+" + result.reward() + " LC", true)
.addField("Новый баланс", result.cashAfter() + " LC", true);
} else {
embed.withDescription("❌ Неверно! Правильный ответ: **" + result.count() + "**\n\n```\n"
+ String.join("\n", result.grid()) + "\n```");
}
context.getChannel().sendMessageEmbeds(embed.build()).queue();
} catch (NumberFormatException e) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Неверный аргумент. Используй `!fish easy/normal/hard` или `!fish <число>`")
).queue();
}
}
.withColor(new Color(0, 150, 255))
.addField("Вопрос", "Сколько **" + result.targetFish() + "** ты видишь?", false)
.withFooter("Просто напиши число в чат")
.build())
.queue();
}
}

View file

@ -1,146 +1,177 @@
package creator.loki.feature.economy.command.minigame;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.feature.economy.service.JobService;
import creator.loki.embed.EmbedFactory;
import creator.loki.embed.EmbedTemplates;
import java.awt.Color;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import creator.loki.embed.EmbedTemplates;
import creator.loki.feature.economy.service.JobService;
public class JobCommand implements Command {
private final JobService jobService;
private final JobService jobService;
public JobCommand(JobService jobService) {
this.jobService = jobService;
public JobCommand(JobService jobService) {
this.jobService = jobService;
}
@Override
public String getName() {
return "job";
}
@Override
public String getDescription() {
return "Управление работой — список, присоединиться";
}
@Override
public String getUsage() {
return "job — текущая работа\njob list — список работ\njob join <название> — устроиться";
}
@Override
public String getCategory() {
return "Экономика";
}
@Override
public void execute(CommandContext context) {
var args = context.getArgs();
var userId = context.getAuthor().getId();
if (args.isEmpty()) {
handleCurrent(context, userId);
} else if (args.get(0).equalsIgnoreCase("list")) {
handleList(context);
} else if (args.get(0).equalsIgnoreCase("join")) {
if (args.size() < 2) {
context
.getChannel()
.sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Укажи название работы: `!job join <название>`"))
.queue();
return;
}
handleJoin(context, userId, args.get(1));
} else {
context
.getChannel()
.sendMessageEmbeds(
EmbedTemplates.warning(
"Работа", "Используй: `!job`, `!job list`, `!job join <название>`"))
.queue();
}
}
private void handleCurrent(CommandContext context, String userId) {
String current = jobService.getCurrentJob(userId);
if (current == null) {
context
.getChannel()
.sendMessageEmbeds(
EmbedTemplates.info(
"Работа", "У тебя нет работы. Используй `!job list` чтобы посмотреть доступные."))
.queue();
return;
}
@Override
public String getName() {
return "job";
JobService.JobInfo info = findJob(current);
if (info == null) {
context
.getChannel()
.sendMessageEmbeds(
EmbedTemplates.warning("Работа", "Текущая работа: **" + current + "**"))
.queue();
return;
}
@Override
public String getDescription() {
return "Управление работой — список, присоединиться";
}
@Override
public String getUsage() {
return "job — текущая работа\njob list — список работ\njob join <название> — устроиться";
}
@Override
public String getCategory() {
return "Экономика";
}
@Override
public void execute(CommandContext context) {
var args = context.getArgs();
var userId = context.getAuthor().getId();
if (args.isEmpty()) {
handleCurrent(context, userId);
} else if (args.get(0).equalsIgnoreCase("list")) {
handleList(context);
} else if (args.get(0).equalsIgnoreCase("join")) {
if (args.size() < 2) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Укажи название работы: `!job join <название>`")
).queue();
return;
}
handleJoin(context, userId, args.get(1));
} else {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.warning("Работа", "Используй: `!job`, `!job list`, `!job join <название>`")
).queue();
}
}
private void handleCurrent(CommandContext context, String userId) {
String current = jobService.getCurrentJob(userId);
if (current == null) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.info("Работа", "У тебя нет работы. Используй `!job list` чтобы посмотреть доступные.")
).queue();
return;
}
JobService.JobInfo info = findJob(current);
if (info == null) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.warning("Работа", "Текущая работа: **" + current + "**")
).queue();
return;
}
context.getChannel().sendMessageEmbeds(
context
.getChannel()
.sendMessageEmbeds(
EmbedFactory.create()
.withTitle("💼 Твоя работа")
.withColor(new Color(100, 200, 255))
.addField("Должность", info.emoji() + " " + info.displayName(), false)
.addField("Зарплата", info.minPayout() + " – " + info.maxPayout() + " LC", true)
.addField("Требуемый уровень", String.valueOf(info.requiredLevel()), true)
.build()
).queue();
.build())
.queue();
}
private void handleList(CommandContext context) {
var jobs = jobService.listJobs();
var desc = new StringBuilder();
for (var job : jobs) {
desc.append(job.emoji())
.append(" **")
.append(job.displayName())
.append("** — ")
.append(job.minPayout())
.append("–")
.append(job.maxPayout())
.append(" LC (ур. ")
.append(job.requiredLevel())
.append(")\n");
}
private void handleList(CommandContext context) {
var jobs = jobService.listJobs();
var desc = new StringBuilder();
for (var job : jobs) {
desc.append(job.emoji()).append(" **").append(job.displayName())
.append("** — ").append(job.minPayout()).append("–").append(job.maxPayout())
.append(" LC (ур. ").append(job.requiredLevel()).append(")\n");
}
context.getChannel().sendMessageEmbeds(
context
.getChannel()
.sendMessageEmbeds(
EmbedFactory.create()
.withTitle("📋 Доступные работы")
.withDescription(desc.toString())
.withColor(new Color(100, 200, 255))
.withFooter("!job join <название> — устроиться на работу")
.build()
).queue();
.build())
.queue();
}
private void handleJoin(CommandContext context, String userId, String name) {
var jobs = jobService.listJobs();
JobService.JobInfo target = null;
for (var job : jobs) {
if (job.name().equalsIgnoreCase(name) || job.displayName().equalsIgnoreCase(name)) {
target = job;
break;
}
}
private void handleJoin(CommandContext context, String userId, String name) {
var jobs = jobService.listJobs();
JobService.JobInfo target = null;
for (var job : jobs) {
if (job.name().equalsIgnoreCase(name) || job.displayName().equalsIgnoreCase(name)) {
target = job;
break;
}
}
if (target == null) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Работа **" + name + "** не найдена. Используй `!job list`")
).queue();
return;
}
jobService.setJob(userId, target.name());
context.getChannel().sendMessageEmbeds(
EmbedTemplates.success("Устройство на работу",
"Ты устроился на работу **" + target.emoji() + " " + target.displayName() + "**!\n"
+ "Используй `!work` чтобы начать работать.")
).queue();
if (target == null) {
context
.getChannel()
.sendMessageEmbeds(
EmbedTemplates.error(
"Ошибка", "Работа **" + name + "** не найдена. Используй `!job list`"))
.queue();
return;
}
private JobService.JobInfo findJob(String name) {
for (var job : jobService.listJobs()) {
if (job.name().equalsIgnoreCase(name) || job.displayName().equalsIgnoreCase(name)) {
return job;
}
}
return null;
jobService.setJob(userId, target.name());
context
.getChannel()
.sendMessageEmbeds(
EmbedTemplates.success(
"Устройство на работу",
"Ты устроился на работу **"
+ target.emoji()
+ " "
+ target.displayName()
+ "**!\n"
+ "Используй `!work` чтобы начать работать."))
.queue();
}
private JobService.JobInfo findJob(String name) {
for (var job : jobService.listJobs()) {
if (job.name().equalsIgnoreCase(name) || job.displayName().equalsIgnoreCase(name)) {
return job;
}
}
return null;
}
}

View file

@ -1,96 +1,109 @@
package creator.loki.feature.economy.command.minigame;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.feature.economy.service.EconomyService;
import creator.loki.feature.economy.service.SlotService;
import creator.loki.embed.EmbedFactory;
import creator.loki.embed.EmbedTemplates;
import java.awt.Color;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import creator.loki.embed.EmbedTemplates;
import creator.loki.feature.economy.service.EconomyService;
import creator.loki.feature.economy.service.SlotService;
public class SlotCommand implements Command {
private final SlotService slotService;
private final EconomyService economyService;
private final SlotService slotService;
private final EconomyService economyService;
public SlotCommand(SlotService slotService, EconomyService economyService) {
this.slotService = slotService;
this.economyService = economyService;
public SlotCommand(SlotService slotService, EconomyService economyService) {
this.slotService = slotService;
this.economyService = economyService;
}
@Override
public String getName() {
return "slot";
}
@Override
public String getDescription() {
return "Крути слот-машину и выигрывай";
}
@Override
public String getUsage() {
return "slot <ставка>";
}
@Override
public String getCategory() {
return "Экономика";
}
@Override
public void execute(CommandContext context) {
var args = context.getArgs();
var userId = context.getAuthor().getId();
if (args.isEmpty()) {
context
.getChannel()
.sendMessageEmbeds(EmbedTemplates.warning("Слоты", "Используй: `!slot <ставка>`"))
.queue();
return;
}
@Override
public String getName() {
return "slot";
long bet;
try {
bet = Long.parseLong(args.get(0));
if (bet <= 0) throw new NumberFormatException();
} catch (NumberFormatException e) {
context
.getChannel()
.sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Ставка должна быть положительным числом!"))
.queue();
return;
}
@Override
public String getDescription() {
return "Крути слот-машину и выигрывай";
var account = economyService.getAccount(userId);
if (account.getCash() < bet) {
context
.getChannel()
.sendMessageEmbeds(
EmbedTemplates.error(
"Ошибка", "Недостаточно средств! Твой баланс: " + account.getCash() + " LC"))
.queue();
return;
}
@Override
public String getUsage() {
return "slot <ставка>";
SlotService.SlotResult result = slotService.spin(userId, bet);
var comboDisplay = result.emoji1() + " | " + result.emoji2() + " | " + result.emoji3();
var desc = new StringBuilder("**" + comboDisplay + "**\n");
if (result.multiplier() > 0) {
desc.append("\n🎰 **")
.append(result.comboName())
.append("!**")
.append(" x")
.append(result.multiplier())
.append("\n\n+**")
.append(result.winAmount())
.append("** LC");
} else {
desc.append("\n\n😔 Повезёт в следующий раз!");
}
@Override
public String getCategory() {
return "Экономика";
}
desc.append("\n\nБаланс: **").append(result.cashAfter()).append("** LC");
@Override
public void execute(CommandContext context) {
var args = context.getArgs();
var userId = context.getAuthor().getId();
if (args.isEmpty()) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.warning("Слоты", "Используй: `!slot <ставка>`")
).queue();
return;
}
long bet;
try {
bet = Long.parseLong(args.get(0));
if (bet <= 0) throw new NumberFormatException();
} catch (NumberFormatException e) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Ставка должна быть положительным числом!")
).queue();
return;
}
var account = economyService.getAccount(userId);
if (account.getCash() < bet) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Недостаточно средств! Твой баланс: " + account.getCash() + " LC")
).queue();
return;
}
SlotService.SlotResult result = slotService.spin(userId, bet);
var comboDisplay = result.emoji1() + " | " + result.emoji2() + " | " + result.emoji3();
var desc = new StringBuilder("**" + comboDisplay + "**\n");
if (result.multiplier() > 0) {
desc.append("\n🎰 **").append(result.comboName()).append("!**")
.append(" x").append(result.multiplier())
.append("\n\n+**").append(result.winAmount()).append("** LC");
} else {
desc.append("\n\n😔 Повезёт в следующий раз!");
}
desc.append("\n\nБаланс: **").append(result.cashAfter()).append("** LC");
context.getChannel().sendMessageEmbeds(
context
.getChannel()
.sendMessageEmbeds(
EmbedFactory.create()
.withTitle("🎰 Слот-машина")
.withDescription(desc.toString())
.withColor(new Color(255, 215, 0))
.build()
).queue();
}
.build())
.queue();
}
}

View file

@ -1,78 +1,86 @@
package creator.loki.feature.economy.command.minigame;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.feature.economy.service.JobService;
import creator.loki.embed.EmbedFactory;
import creator.loki.embed.EmbedTemplates;
import java.awt.Color;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import creator.loki.embed.EmbedTemplates;
import creator.loki.feature.economy.service.JobService;
public class WorkCommand implements Command {
private final JobService jobService;
private final JobService jobService;
public WorkCommand(JobService jobService) {
this.jobService = jobService;
public WorkCommand(JobService jobService) {
this.jobService = jobService;
}
@Override
public String getName() {
return "work";
}
@Override
public String getDescription() {
return "Работать и зарабатывать деньги";
}
@Override
public String getCategory() {
return "Экономика";
}
@Override
public void execute(CommandContext context) {
var userId = context.getAuthor().getId();
String currentJob = jobService.getCurrentJob(userId);
if (currentJob == null) {
context
.getChannel()
.sendMessageEmbeds(
EmbedTemplates.warning(
"Работа", "У тебя нет работы! Используй `!job list` чтобы выбрать."))
.queue();
return;
}
@Override
public String getName() {
return "work";
JobService.JobInfo info = findJob(currentJob);
JobService.WorkResult result = jobService.work(userId, 0);
if (result == null) {
context
.getChannel()
.sendMessageEmbeds(
EmbedTemplates.error(
"Ошибка", "⏳ Ты уже работал сегодня! Отдохни и возвращайся завтра."))
.queue();
return;
}
@Override
public String getDescription() {
return "Работать и зарабатывать деньги";
}
String emoji = info != null ? info.emoji() : "💼";
String jobName = info != null ? info.displayName() : currentJob;
@Override
public String getCategory() {
return "Экономика";
}
@Override
public void execute(CommandContext context) {
var userId = context.getAuthor().getId();
String currentJob = jobService.getCurrentJob(userId);
if (currentJob == null) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.warning("Работа", "У тебя нет работы! Используй `!job list` чтобы выбрать.")
).queue();
return;
}
JobService.JobInfo info = findJob(currentJob);
JobService.WorkResult result = jobService.work(userId, 0);
if (result == null) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "⏳ Ты уже работал сегодня! Отдохни и возвращайся завтра.")
).queue();
return;
}
String emoji = info != null ? info.emoji() : "💼";
String jobName = info != null ? info.displayName() : currentJob;
context.getChannel().sendMessageEmbeds(
context
.getChannel()
.sendMessageEmbeds(
EmbedFactory.create()
.withTitle("💼 Работа завершена")
.withColor(new Color(100, 200, 255))
.addField("Должность", emoji + " " + jobName, false)
.addField("Заработано", "+**" + result.amount() + "** LC", true)
.addField("Новый баланс", "**" + result.newCash() + "** LC", true)
.build()
).queue();
}
.build())
.queue();
}
private JobService.JobInfo findJob(String name) {
for (var job : jobService.listJobs()) {
if (job.name().equalsIgnoreCase(name) || job.displayName().equalsIgnoreCase(name)) {
return job;
}
}
return null;
private JobService.JobInfo findJob(String name) {
for (var job : jobService.listJobs()) {
if (job.name().equalsIgnoreCase(name) || job.displayName().equalsIgnoreCase(name)) {
return job;
}
}
return null;
}
}

View file

@ -1,107 +1,113 @@
package creator.loki.feature.economy.command.social;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.core.security.CooldownManager;
import creator.loki.feature.economy.service.EconomyService;
import creator.loki.embed.EmbedFactory;
import creator.loki.embed.EmbedTemplates;
import net.dv8tion.jda.api.entities.Member;
import java.awt.Color;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.core.security.CooldownManager;
import creator.loki.embed.EmbedFactory;
import creator.loki.embed.EmbedTemplates;
import creator.loki.feature.economy.service.EconomyService;
import net.dv8tion.jda.api.entities.Member;
public class RobCommand implements Command {
private final EconomyService economyService;
private final CooldownManager cooldownManager;
private final Random random;
private final EconomyService economyService;
private final CooldownManager cooldownManager;
private final Random random;
public RobCommand(EconomyService economyService, CooldownManager cooldownManager) {
this.economyService = economyService;
this.cooldownManager = cooldownManager;
this.random = ThreadLocalRandom.current();
}
public RobCommand(EconomyService economyService, CooldownManager cooldownManager) {
this.economyService = economyService;
this.cooldownManager = cooldownManager;
this.random = ThreadLocalRandom.current();
}
@Override
public String getName() {
return "rob";
}
@Override
public String getName() {
return "rob";
}
@Override
public String getDescription() {
return "Ограбить пользователя";
}
@Override
public String getDescription() {
return "Ограбить пользователя";
}
@Override
public String getUsage() {
return "rob <@user>";
}
@Override
public String getUsage() {
return "rob <@user>";
}
@Override
public String getCategory() {
return "Экономика";
}
@Override
public String getCategory() {
return "Экономика";
}
@Override
public void execute(CommandContext ctx) {
List<Member> mentioned = ctx.getMessage().getMentions().getMembers();
if (mentioned.isEmpty()) {
ctx.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Укажите пользователя: " + getUsage())
).queue();
return;
}
Member target = mentioned.get(0);
String uid = ctx.getAuthor().getId();
String tid = target.getId();
if (tid.equals(uid)) {
ctx.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Нельзя ограбить самого себя!")
).queue();
return;
}
if (cooldownManager.isOnCooldown(uid, "rob")) {
long rem = cooldownManager.getRemainingSeconds(uid, "rob");
ctx.getChannel().sendMessageEmbeds(EmbedTemplates.warning("Кдн",
"Подожди ещё **" + (rem / 3600) + "ч " + ((rem % 3600) / 60) + "м**")
).queue();
return;
}
cooldownManager.applyCooldown(uid, "rob", 86400);
var sender = economyService.getAccount(uid);
var victim = economyService.getAccount(tid);
if (sender.getCash() <= 0 || victim.getCash() <= 0) {
ctx.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "У одного из вас нет наличных!")
).queue();
return;
}
if (random.nextDouble() < 0.4) {
long steal = Math.min(Math.min((long) (victim.getCash() * 0.15), 10000L), victim.getCash());
if (steal <= 0) {
ctx.getChannel().sendMessageEmbeds(EmbedTemplates.info("Ограбление", "Слишком мелкая цель...")).queue();
return;
}
economyService.transfer(tid, uid, steal);
ctx.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.withTitle("🔫 Ограбление удалось!")
.withDescription("Ты украл **" + steal + "** LC у " + target.getAsMention())
.withColor(new Color(46, 204, 113))
.build()
).queue();
} else {
long penalty = Math.min(Math.min((long) (sender.getCash() * 0.10), 5000L), sender.getCash());
if (penalty > 0) economyService.transfer(uid, tid, penalty);
ctx.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.withTitle("👮 Ограбление провалено!")
.withDescription("Ты заплатил **" + penalty + "** LC " + target.getAsMention())
.withColor(new Color(231, 76, 60))
.build()
).queue();
}
@Override
public void execute(CommandContext ctx) {
List<Member> mentioned = ctx.getMessage().getMentions().getMembers();
if (mentioned.isEmpty()) {
ctx.getChannel()
.sendMessageEmbeds(EmbedTemplates.error("Ошибка", "Укажите пользователя: " + getUsage()))
.queue();
return;
}
Member target = mentioned.get(0);
String uid = ctx.getAuthor().getId();
String tid = target.getId();
if (tid.equals(uid)) {
ctx.getChannel()
.sendMessageEmbeds(EmbedTemplates.error("Ошибка", "Нельзя ограбить самого себя!"))
.queue();
return;
}
if (cooldownManager.isOnCooldown(uid, "rob")) {
long rem = cooldownManager.getRemainingSeconds(uid, "rob");
ctx.getChannel()
.sendMessageEmbeds(
EmbedTemplates.warning(
"Кдн", "Подожди ещё **" + (rem / 3600) + "ч " + ((rem % 3600) / 60) + "м**"))
.queue();
return;
}
cooldownManager.applyCooldown(uid, "rob", 86400);
var sender = economyService.getAccount(uid);
var victim = economyService.getAccount(tid);
if (sender.getCash() <= 0 || victim.getCash() <= 0) {
ctx.getChannel()
.sendMessageEmbeds(EmbedTemplates.error("Ошибка", "У одного из вас нет наличных!"))
.queue();
return;
}
if (random.nextDouble() < 0.4) {
long steal = Math.min(Math.min((long) (victim.getCash() * 0.15), 10000L), victim.getCash());
if (steal <= 0) {
ctx.getChannel()
.sendMessageEmbeds(EmbedTemplates.info("Ограбление", "Слишком мелкая цель..."))
.queue();
return;
}
economyService.transfer(tid, uid, steal);
ctx.getChannel()
.sendMessageEmbeds(
EmbedFactory.create()
.withTitle("🔫 Ограбление удалось!")
.withDescription("Ты украл **" + steal + "** LC у " + target.getAsMention())
.withColor(new Color(46, 204, 113))
.build())
.queue();
} else {
long penalty = Math.min(Math.min((long) (sender.getCash() * 0.10), 5000L), sender.getCash());
if (penalty > 0) economyService.transfer(uid, tid, penalty);
ctx.getChannel()
.sendMessageEmbeds(
EmbedFactory.create()
.withTitle("👮 Ограбление провалено!")
.withDescription("Ты заплатил **" + penalty + "** LC " + target.getAsMention())
.withColor(new Color(231, 76, 60))
.build())
.queue();
}
}
}

View file

@ -3,237 +3,140 @@ package creator.loki.feature.economy.domain;
import java.util.Objects;
public class EconomyAccount {
private final String userId;
private final long cash;
private final long bank;
private final long totalEarned;
private final long totalSpent;
private final String lastDaily;
private final int dailyStreak;
private final int fishingLuck;
private final int efficiency;
private final String lastFish;
private final String job;
private final String lastWork;
private final String userId;
private final long cash;
private final long bank;
private final long totalEarned;
private final long totalSpent;
private final String lastDaily;
private final int dailyStreak;
private final int fishingLuck;
private final int efficiency;
private final String lastFish;
private final String job;
private final String lastWork;
public EconomyAccount(String userId) {
this.userId = userId;
this.cash = 0;
this.bank = 0;
this.totalEarned = 0;
this.totalSpent = 0;
this.lastDaily = null;
this.dailyStreak = 0;
this.fishingLuck = 0;
this.efficiency = 0;
this.lastFish = null;
this.job = null;
this.lastWork = null;
}
public EconomyAccount(String userId) {
this.userId = userId;
this.cash = 0;
this.bank = 0;
this.totalEarned = 0;
this.totalSpent = 0;
this.lastDaily = null;
this.dailyStreak = 0;
this.fishingLuck = 0;
this.efficiency = 0;
this.lastFish = null;
this.job = null;
this.lastWork = null;
}
private EconomyAccount(Builder builder) {
this.userId = builder.userId;
this.cash = builder.cash;
this.bank = builder.bank;
this.totalEarned = builder.totalEarned;
this.totalSpent = builder.totalSpent;
this.lastDaily = builder.lastDaily;
this.dailyStreak = builder.dailyStreak;
this.fishingLuck = builder.fishingLuck;
this.efficiency = builder.efficiency;
this.lastFish = builder.lastFish;
this.job = builder.job;
this.lastWork = builder.lastWork;
}
EconomyAccount(EconomyAccountBuilder builder) {
this.userId = builder.userId;
this.cash = builder.cash;
this.bank = builder.bank;
this.totalEarned = builder.totalEarned;
this.totalSpent = builder.totalSpent;
this.lastDaily = builder.lastDaily;
this.dailyStreak = builder.dailyStreak;
this.fishingLuck = builder.fishingLuck;
this.efficiency = builder.efficiency;
this.lastFish = builder.lastFish;
this.job = builder.job;
this.lastWork = builder.lastWork;
}
public String getUserId() {
return userId;
}
public String getUserId() {
return userId;
}
public long getCash() {
return cash;
}
public long getCash() {
return cash;
}
public long getBank() {
return bank;
}
public long getBank() {
return bank;
}
public long getTotalEarned() {
return totalEarned;
}
public long getTotalEarned() {
return totalEarned;
}
public long getTotalSpent() {
return totalSpent;
}
public long getTotalSpent() {
return totalSpent;
}
public String getLastDaily() {
return lastDaily;
}
public String getLastDaily() {
return lastDaily;
}
public int getDailyStreak() {
return dailyStreak;
}
public int getDailyStreak() {
return dailyStreak;
}
public int getFishingLuck() {
return fishingLuck;
}
public int getFishingLuck() {
return fishingLuck;
}
public int getEfficiency() {
return efficiency;
}
public int getEfficiency() {
return efficiency;
}
public String getLastFish() {
return lastFish;
}
public String getLastFish() {
return lastFish;
}
public String getJob() {
return job;
}
public String getJob() {
return job;
}
public String getLastWork() {
return lastWork;
}
public String getLastWork() {
return lastWork;
}
public long getTotalBalance() {
return cash + bank;
}
public long getTotalBalance() {
return cash + bank;
}
public EconomyAccount withCash(long cash) {
return new Builder(this).cash(cash).build();
}
public EconomyAccount withCash(long cash) {
return new EconomyAccountBuilder(this).cash(cash).build();
}
public EconomyAccount withBank(long bank) {
return new Builder(this).bank(bank).build();
}
public EconomyAccount withBank(long bank) {
return new EconomyAccountBuilder(this).bank(bank).build();
}
public EconomyAccount withAddedEarned(long amount) {
return new Builder(this).totalEarned(this.totalEarned + amount).build();
}
public EconomyAccount withAddedEarned(long amount) {
return new EconomyAccountBuilder(this).totalEarned(this.totalEarned + amount).build();
}
public EconomyAccount withAddedSpent(long amount) {
return new Builder(this).totalSpent(this.totalSpent + amount).build();
}
public EconomyAccount withAddedSpent(long amount) {
return new EconomyAccountBuilder(this).totalSpent(this.totalSpent + amount).build();
}
public EconomyAccount withFishingLuck(int level) {
return new Builder(this).fishingLuck(level).build();
}
public EconomyAccount withFishingLuck(int level) {
return new EconomyAccountBuilder(this).fishingLuck(level).build();
}
public EconomyAccount withEfficiency(int level) {
return new Builder(this).efficiency(level).build();
}
public EconomyAccount withEfficiency(int level) {
return new EconomyAccountBuilder(this).efficiency(level).build();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
EconomyAccount that = (EconomyAccount) o;
return Objects.equals(userId, that.userId);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
EconomyAccount that = (EconomyAccount) o;
return Objects.equals(userId, that.userId);
}
@Override
public int hashCode() {
return Objects.hash(userId);
}
@Override
public int hashCode() {
return Objects.hash(userId);
}
@Override
public String toString() {
return String.format("EconomyAccount[%s: Cash %d, Bank %d]",
userId, cash, bank);
}
public static class Builder {
private String userId;
private long cash;
private long bank;
private long totalEarned;
private long totalSpent;
private String lastDaily;
private int dailyStreak;
private int fishingLuck;
private int efficiency;
private String lastFish;
private String job;
private String lastWork;
public Builder() {}
public Builder(EconomyAccount existing) {
this.userId = existing.userId;
this.cash = existing.cash;
this.bank = existing.bank;
this.totalEarned = existing.totalEarned;
this.totalSpent = existing.totalSpent;
this.lastDaily = existing.lastDaily;
this.dailyStreak = existing.dailyStreak;
this.fishingLuck = existing.fishingLuck;
this.efficiency = existing.efficiency;
this.lastFish = existing.lastFish;
this.job = existing.job;
this.lastWork = existing.lastWork;
}
public Builder userId(String userId) {
this.userId = userId;
return this;
}
public Builder cash(long cash) {
this.cash = cash;
return this;
}
public Builder bank(long bank) {
this.bank = bank;
return this;
}
public Builder totalEarned(long totalEarned) {
this.totalEarned = totalEarned;
return this;
}
public Builder totalSpent(long totalSpent) {
this.totalSpent = totalSpent;
return this;
}
public Builder lastDaily(String lastDaily) {
this.lastDaily = lastDaily;
return this;
}
public Builder dailyStreak(int dailyStreak) {
this.dailyStreak = dailyStreak;
return this;
}
public Builder fishingLuck(int fishingLuck) {
this.fishingLuck = fishingLuck;
return this;
}
public Builder efficiency(int efficiency) {
this.efficiency = efficiency;
return this;
}
public Builder lastFish(String lastFish) {
this.lastFish = lastFish;
return this;
}
public Builder job(String job) {
this.job = job;
return this;
}
public Builder lastWork(String lastWork) {
this.lastWork = lastWork;
return this;
}
public EconomyAccount build() {
return new EconomyAccount(this);
}
}
@Override
public String toString() {
return String.format("EconomyAccount[%s: Cash %d, Bank %d]", userId, cash, bank);
}
}

View file

@ -0,0 +1,97 @@
package creator.loki.feature.economy.domain;
public class EconomyAccountBuilder {
String userId;
long cash;
long bank;
long totalEarned;
long totalSpent;
String lastDaily;
int dailyStreak;
int fishingLuck;
int efficiency;
String lastFish;
String job;
String lastWork;
public EconomyAccountBuilder() {}
public EconomyAccountBuilder(EconomyAccount existing) {
this.userId = existing.getUserId();
this.cash = existing.getCash();
this.bank = existing.getBank();
this.totalEarned = existing.getTotalEarned();
this.totalSpent = existing.getTotalSpent();
this.lastDaily = existing.getLastDaily();
this.dailyStreak = existing.getDailyStreak();
this.fishingLuck = existing.getFishingLuck();
this.efficiency = existing.getEfficiency();
this.lastFish = existing.getLastFish();
this.job = existing.getJob();
this.lastWork = existing.getLastWork();
}
public EconomyAccountBuilder userId(String userId) {
this.userId = userId;
return this;
}
public EconomyAccountBuilder cash(long cash) {
this.cash = cash;
return this;
}
public EconomyAccountBuilder bank(long bank) {
this.bank = bank;
return this;
}
public EconomyAccountBuilder totalEarned(long totalEarned) {
this.totalEarned = totalEarned;
return this;
}
public EconomyAccountBuilder totalSpent(long totalSpent) {
this.totalSpent = totalSpent;
return this;
}
public EconomyAccountBuilder lastDaily(String lastDaily) {
this.lastDaily = lastDaily;
return this;
}
public EconomyAccountBuilder dailyStreak(int dailyStreak) {
this.dailyStreak = dailyStreak;
return this;
}
public EconomyAccountBuilder fishingLuck(int fishingLuck) {
this.fishingLuck = fishingLuck;
return this;
}
public EconomyAccountBuilder efficiency(int efficiency) {
this.efficiency = efficiency;
return this;
}
public EconomyAccountBuilder lastFish(String lastFish) {
this.lastFish = lastFish;
return this;
}
public EconomyAccountBuilder job(String job) {
this.job = job;
return this;
}
public EconomyAccountBuilder lastWork(String lastWork) {
this.lastWork = lastWork;
return this;
}
public EconomyAccount build() {
return new EconomyAccount(this);
}
}

View file

@ -1,9 +1,4 @@
package creator.loki.feature.economy.domain;
public record EconomyTransaction(
String fromId,
String toId,
long amount,
String type,
long timestamp
) {}
String fromId, String toId, long amount, String type, long timestamp) {}

View file

@ -1,53 +1,53 @@
package creator.loki.feature.economy.domain;
public enum Job {
WOODCUTTER("Лесоруб", 0, 15, 30, "\uD83E\uDE93"),
MINER("Шахтёр", 0, 20, 40, "\u26CF\uFE0F"),
FISHERMAN("Рыбак", 0, 25, 50, "\uD83C\uDFA3"),
COOK("Повар", 15, 30, 60, "\uD83C\uDF73"),
PROGRAMMER("Программист", 20, 40, 80, "\uD83D\uDCBB"),
ASTRONAUT("Космонавт", 35, 50, 100, "\uD83E\uDEB8");
WOODCUTTER("Лесоруб", 0, 15, 30, "\uD83E\uDE93"),
MINER("Шахтёр", 0, 20, 40, "\u26CF\uFE0F"),
FISHERMAN("Рыбак", 0, 25, 50, "\uD83C\uDFA3"),
COOK("Повар", 15, 30, 60, "\uD83C\uDF73"),
PROGRAMMER("Программист", 20, 40, 80, "\uD83D\uDCBB"),
ASTRONAUT("Космонавт", 35, 50, 100, "\uD83E\uDEB8");
private final String displayName;
private final int requiredLevel;
private final int minPayout;
private final int maxPayout;
private final String emoji;
private final String displayName;
private final int requiredLevel;
private final int minPayout;
private final int maxPayout;
private final String emoji;
Job(String displayName, int requiredLevel, int minPayout, int maxPayout, String emoji) {
this.displayName = displayName;
this.requiredLevel = requiredLevel;
this.minPayout = minPayout;
this.maxPayout = maxPayout;
this.emoji = emoji;
}
public String getDisplayName() {
return displayName;
}
public int getRequiredLevel() {
return requiredLevel;
}
public int getMinPayout() {
return minPayout;
}
public int getMaxPayout() {
return maxPayout;
}
public String getEmoji() {
return emoji;
}
public static Job fromName(String name) {
for (Job j : values()) {
if (j.name().equalsIgnoreCase(name) || j.getDisplayName().equalsIgnoreCase(name)) {
return j;
}
}
return null;
Job(String displayName, int requiredLevel, int minPayout, int maxPayout, String emoji) {
this.displayName = displayName;
this.requiredLevel = requiredLevel;
this.minPayout = minPayout;
this.maxPayout = maxPayout;
this.emoji = emoji;
}
public String getDisplayName() {
return displayName;
}
public int getRequiredLevel() {
return requiredLevel;
}
public int getMinPayout() {
return minPayout;
}
public int getMaxPayout() {
return maxPayout;
}
public String getEmoji() {
return emoji;
}
public static Job fromName(String name) {
for (Job j : values()) {
if (j.name().equalsIgnoreCase(name) || j.getDisplayName().equalsIgnoreCase(name)) {
return j;
}
}
return null;
}
}

View file

@ -1,37 +1,39 @@
package creator.loki.feature.economy.domain;
public enum SlotCombination {
CHERRY(new String[]{"\uD83C\uDF52", "\uD83C\uDF52", "\uD83C\uDF52"}, 3, 10.0),
BELL(new String[]{"\uD83D\uDD14", "\uD83D\uDD14", "\uD83D\uDD14"}, 5, 5.0),
DIAMOND(new String[]{"\uD83D\uDC8E", "\uD83D\uDC8E", "\uD83D\uDC8E"}, 10, 2.0),
CROWN(new String[]{"\uD83D\uDC51", "\uD83D\uDC51", "\uD83D\uDC51"}, 15, 1.0),
JACKPOT(new String[]{"\u26A1", "\u26A1", "\u26A1"}, 50, 0.1);
CHERRY(new String[] {"\uD83C\uDF52", "\uD83C\uDF52", "\uD83C\uDF52"}, 3, 10.0),
BELL(new String[] {"\uD83D\uDD14", "\uD83D\uDD14", "\uD83D\uDD14"}, 5, 5.0),
DIAMOND(new String[] {"\uD83D\uDC8E", "\uD83D\uDC8E", "\uD83D\uDC8E"}, 10, 2.0),
CROWN(new String[] {"\uD83D\uDC51", "\uD83D\uDC51", "\uD83D\uDC51"}, 15, 1.0),
JACKPOT(new String[] {"\u26A1", "\u26A1", "\u26A1"}, 50, 0.1);
private final String[] emojis;
private final int multiplier;
private final double chance;
private final String[] emojis;
private final int multiplier;
private final double chance;
SlotCombination(String[] emojis, int multiplier, double chance) {
this.emojis = emojis;
this.multiplier = multiplier;
this.chance = chance;
}
SlotCombination(String[] emojis, int multiplier, double chance) {
this.emojis = emojis;
this.multiplier = multiplier;
this.chance = chance;
}
public String[] getEmojis() {
return emojis;
}
public int getMultiplier() {
return multiplier;
}
public double getChance() {
return chance;
}
public String[] getEmojis() {
return emojis;
}
public static SlotCombination match(String e1, String e2, String e3) {
if (!e1.equals(e2) || !e2.equals(e3)) return null;
for (SlotCombination sc : values()) {
if (sc.emojis[0].equals(e1)) return sc;
}
return null;
public int getMultiplier() {
return multiplier;
}
public double getChance() {
return chance;
}
public static SlotCombination match(String e1, String e2, String e3) {
if (!e1.equals(e2) || !e2.equals(e3)) return null;
for (SlotCombination sc : values()) {
if (sc.emojis[0].equals(e1)) return sc;
}
return null;
}
}

View file

@ -1,17 +1,20 @@
package creator.loki.feature.economy.repository;
import creator.loki.feature.economy.domain.EconomyAccount;
import java.util.List;
import java.util.Optional;
public interface EconomyRepository {
Optional<EconomyAccount> findById(String userId);
void save(EconomyAccount account);
boolean exists(String userId);
List<EconomyAccount> findTopByBalance(int limit);
import creator.loki.feature.economy.domain.EconomyAccount;
default EconomyAccount getAccount(String userId) {
return findById(userId).orElse(new EconomyAccount(userId));
}
public interface EconomyRepository {
Optional<EconomyAccount> findById(String userId);
void save(EconomyAccount account);
boolean exists(String userId);
List<EconomyAccount> findTopByBalance(int limit);
default EconomyAccount getAccount(String userId) {
return findById(userId).orElse(new EconomyAccount(userId));
}
}

View file

@ -1,10 +1,5 @@
package creator.loki.feature.economy.repository;
import creator.loki.database.Database;
import creator.loki.feature.economy.domain.EconomyAccount;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
@ -13,18 +8,25 @@ import java.util.Collections;
import java.util.List;
import java.util.Optional;
import creator.loki.database.Database;
import creator.loki.feature.economy.domain.EconomyAccount;
import creator.loki.feature.economy.domain.EconomyAccountBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SqlEconomyRepository implements EconomyRepository {
private static final Logger logger = LoggerFactory.getLogger(SqlEconomyRepository.class);
private static final int MAX_TOP = 100;
private final Database database;
private static final Logger logger = LoggerFactory.getLogger(SqlEconomyRepository.class);
private static final int MAX_TOP = 100;
private final Database database;
public SqlEconomyRepository(Database database) {
this.database = database;
}
public SqlEconomyRepository(Database database) {
this.database = database;
}
@Override
public Optional<EconomyAccount> findById(String userId) {
String sql = """
@Override
public Optional<EconomyAccount> findById(String userId) {
String sql =
"""
SELECT user_id, cash, bank, total_earned, total_spent,
last_daily, daily_streak, fishing_luck, efficiency,
last_fish, job, last_work
@ -32,22 +34,23 @@ public class SqlEconomyRepository implements EconomyRepository {
WHERE user_id = ?
""";
try (PreparedStatement stmt = database.prepare(sql)) {
stmt.setString(1, userId);
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
return Optional.of(mapResultSetToAccount(rs));
}
}
} catch (SQLException e) {
logger.error("Error fetching economy account for {}", userId, e);
try (PreparedStatement stmt = database.prepare(sql)) {
stmt.setString(1, userId);
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
return Optional.of(mapResultSetToAccount(rs));
}
return Optional.empty();
}
} catch (SQLException e) {
logger.error("Error fetching economy account for {}", userId, e);
}
return Optional.empty();
}
@Override
public void save(EconomyAccount account) {
String sql = """
@Override
public void save(EconomyAccount account) {
String sql =
"""
INSERT INTO economy (user_id, cash, bank, total_earned, total_spent,
last_daily, daily_streak, fishing_luck, efficiency,
last_fish, job, last_work)
@ -67,59 +70,60 @@ public class SqlEconomyRepository implements EconomyRepository {
last_work = excluded.last_work
""";
try (PreparedStatement stmt = database.prepare(sql)) {
stmt.setString(1, account.getUserId());
stmt.setLong(2, account.getCash());
stmt.setLong(3, account.getBank());
stmt.setLong(4, account.getTotalEarned());
stmt.setLong(5, account.getTotalSpent());
if (account.getLastDaily() != null) {
stmt.setString(6, account.getLastDaily());
} else {
stmt.setNull(6, java.sql.Types.VARCHAR);
}
stmt.setInt(7, account.getDailyStreak());
stmt.setInt(8, account.getFishingLuck());
stmt.setInt(9, account.getEfficiency());
if (account.getLastFish() != null) {
stmt.setString(10, account.getLastFish());
} else {
stmt.setNull(10, java.sql.Types.VARCHAR);
}
if (account.getJob() != null) {
stmt.setString(11, account.getJob());
} else {
stmt.setNull(11, java.sql.Types.VARCHAR);
}
if (account.getLastWork() != null) {
stmt.setString(12, account.getLastWork());
} else {
stmt.setNull(12, java.sql.Types.VARCHAR);
}
stmt.executeUpdate();
} catch (SQLException e) {
logger.error("Error saving economy account for {}", account.getUserId(), e);
}
try (PreparedStatement stmt = database.prepare(sql)) {
stmt.setString(1, account.getUserId());
stmt.setLong(2, account.getCash());
stmt.setLong(3, account.getBank());
stmt.setLong(4, account.getTotalEarned());
stmt.setLong(5, account.getTotalSpent());
if (account.getLastDaily() != null) {
stmt.setString(6, account.getLastDaily());
} else {
stmt.setNull(6, java.sql.Types.VARCHAR);
}
stmt.setInt(7, account.getDailyStreak());
stmt.setInt(8, account.getFishingLuck());
stmt.setInt(9, account.getEfficiency());
if (account.getLastFish() != null) {
stmt.setString(10, account.getLastFish());
} else {
stmt.setNull(10, java.sql.Types.VARCHAR);
}
if (account.getJob() != null) {
stmt.setString(11, account.getJob());
} else {
stmt.setNull(11, java.sql.Types.VARCHAR);
}
if (account.getLastWork() != null) {
stmt.setString(12, account.getLastWork());
} else {
stmt.setNull(12, java.sql.Types.VARCHAR);
}
stmt.executeUpdate();
} catch (SQLException e) {
logger.error("Error saving economy account for {}", account.getUserId(), e);
}
}
@Override
public boolean exists(String userId) {
String sql = "SELECT 1 FROM economy WHERE user_id = ?";
try (PreparedStatement stmt = database.prepare(sql)) {
stmt.setString(1, userId);
try (ResultSet rs = stmt.executeQuery()) {
return rs.next();
}
} catch (SQLException e) {
logger.error("Error checking economy existence for {}", userId, e);
}
return false;
@Override
public boolean exists(String userId) {
String sql = "SELECT 1 FROM economy WHERE user_id = ?";
try (PreparedStatement stmt = database.prepare(sql)) {
stmt.setString(1, userId);
try (ResultSet rs = stmt.executeQuery()) {
return rs.next();
}
} catch (SQLException e) {
logger.error("Error checking economy existence for {}", userId, e);
}
return false;
}
@Override
public List<EconomyAccount> findTopByBalance(int limit) {
int lim = clampLimit(limit);
String sql = """
@Override
public List<EconomyAccount> findTopByBalance(int limit) {
int lim = clampLimit(limit);
String sql =
"""
SELECT user_id, cash, bank, total_earned, total_spent,
last_daily, daily_streak, fishing_luck, efficiency,
last_fish, job, last_work
@ -127,39 +131,39 @@ public class SqlEconomyRepository implements EconomyRepository {
ORDER BY (cash + bank) DESC, cash DESC, user_id ASC
LIMIT ?
""";
List<EconomyAccount> list = new ArrayList<>();
try (PreparedStatement stmt = database.prepare(sql)) {
stmt.setInt(1, lim);
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
list.add(mapResultSetToAccount(rs));
}
}
} catch (SQLException e) {
logger.error("Error fetching top by balance", e);
List<EconomyAccount> list = new ArrayList<>();
try (PreparedStatement stmt = database.prepare(sql)) {
stmt.setInt(1, lim);
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
list.add(mapResultSetToAccount(rs));
}
return Collections.unmodifiableList(list);
}
} catch (SQLException e) {
logger.error("Error fetching top by balance", e);
}
return Collections.unmodifiableList(list);
}
private static int clampLimit(int limit) {
if (limit <= 0) return 1;
return Math.min(limit, MAX_TOP);
}
private static int clampLimit(int limit) {
if (limit <= 0) return 1;
return Math.min(limit, MAX_TOP);
}
private EconomyAccount mapResultSetToAccount(ResultSet rs) throws SQLException {
return new EconomyAccount.Builder()
.userId(rs.getString("user_id"))
.cash(rs.getLong("cash"))
.bank(rs.getLong("bank"))
.totalEarned(rs.getLong("total_earned"))
.totalSpent(rs.getLong("total_spent"))
.lastDaily(rs.getString("last_daily"))
.dailyStreak(rs.getInt("daily_streak"))
.fishingLuck(rs.getInt("fishing_luck"))
.efficiency(rs.getInt("efficiency"))
.lastFish(rs.getString("last_fish"))
.job(rs.getString("job"))
.lastWork(rs.getString("last_work"))
.build();
}
private EconomyAccount mapResultSetToAccount(ResultSet rs) throws SQLException {
return new EconomyAccountBuilder()
.userId(rs.getString("user_id"))
.cash(rs.getLong("cash"))
.bank(rs.getLong("bank"))
.totalEarned(rs.getLong("total_earned"))
.totalSpent(rs.getLong("total_spent"))
.lastDaily(rs.getString("last_daily"))
.dailyStreak(rs.getInt("daily_streak"))
.fishingLuck(rs.getInt("fishing_luck"))
.efficiency(rs.getInt("efficiency"))
.lastFish(rs.getString("last_fish"))
.job(rs.getString("job"))
.lastWork(rs.getString("last_work"))
.build();
}
}

View file

@ -1,141 +1,172 @@
package creator.loki.feature.economy.service;
import creator.loki.feature.economy.domain.EconomyAccount;
import creator.loki.feature.economy.repository.EconomyRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
import creator.loki.feature.economy.domain.EconomyAccount;
import creator.loki.feature.economy.repository.EconomyRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class BlackjackService {
private static final Logger logger = LoggerFactory.getLogger(BlackjackService.class);
private static final Logger logger = LoggerFactory.getLogger(BlackjackService.class);
private static final String[] DECK = {"2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"};
private static final String[] DECK = {
"2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"
};
private final EconomyRepository repository;
private final ConcurrentHashMap<String, ActiveBlackjack> activeGames = new ConcurrentHashMap<>();
private final EconomyRepository repository;
private final ConcurrentHashMap<String, ActiveBlackjack> activeGames = new ConcurrentHashMap<>();
public BlackjackService(EconomyRepository repository) {
this.repository = repository;
public BlackjackService(EconomyRepository repository) {
this.repository = repository;
}
private record ActiveBlackjack(
List<String> playerCards, List<String> dealerCards, long bet, String userId) {}
public BlackjackState startGame(String userId, long bet) {
if (bet <= 0 || activeGames.containsKey(userId)) return null;
EconomyAccount account = repository.getAccount(userId);
if (account.getCash() < bet) return null;
repository.save(account.withCash(account.getCash() - bet));
List<String> playerCards = new ArrayList<>(List.of(drawCard(), drawCard()));
List<String> dealerCards = new ArrayList<>(List.of(drawCard(), drawCard()));
int playerSum = calculateSum(playerCards);
int dealerSum = calculateSum(dealerCards);
boolean playerBj = playerSum == 21;
boolean dealerBj = dealerSum == 21;
if (playerBj || dealerBj) {
long payout;
if (playerBj && !dealerBj) {
payout = (long) (bet * 2.5);
} else {
payout = 0;
}
repository.save(
repository.getAccount(userId).withCash(repository.getAccount(userId).getCash() + payout));
return new BlackjackState(
playerCards.toArray(String[]::new),
dealerCards.toArray(String[]::new),
playerSum,
dealerSum,
bet,
true,
payout);
}
private record ActiveBlackjack(List<String> playerCards, List<String> dealerCards, long bet, String userId) {}
ActiveBlackjack game = new ActiveBlackjack(playerCards, dealerCards, bet, userId);
activeGames.put(userId, game);
return new BlackjackState(
playerCards.toArray(String[]::new),
dealerCards.toArray(String[]::new),
playerSum,
dealerSum,
bet,
false,
0);
}
public BlackjackState startGame(String userId, long bet) {
if (bet <= 0 || activeGames.containsKey(userId)) return null;
EconomyAccount account = repository.getAccount(userId);
if (account.getCash() < bet) return null;
public BlackjackState hit(String userId) {
ActiveBlackjack game = activeGames.get(userId);
if (game == null) return null;
repository.save(account.withCash(account.getCash() - bet));
game.playerCards().add(drawCard());
int sum = calculateSum(game.playerCards());
List<String> playerCards = new ArrayList<>(List.of(drawCard(), drawCard()));
List<String> dealerCards = new ArrayList<>(List.of(drawCard(), drawCard()));
int playerSum = calculateSum(playerCards);
int dealerSum = calculateSum(dealerCards);
boolean playerBj = playerSum == 21;
boolean dealerBj = dealerSum == 21;
if (playerBj || dealerBj) {
long payout;
if (playerBj && !dealerBj) {
payout = (long) (bet * 2.5);
} else {
payout = 0;
}
repository.save(repository.getAccount(userId).withCash(repository.getAccount(userId).getCash() + payout));
return new BlackjackState(
playerCards.toArray(String[]::new), dealerCards.toArray(String[]::new),
playerSum, dealerSum, bet, true, payout
);
}
ActiveBlackjack game = new ActiveBlackjack(playerCards, dealerCards, bet, userId);
activeGames.put(userId, game);
return new BlackjackState(
playerCards.toArray(String[]::new), dealerCards.toArray(String[]::new),
playerSum, dealerSum, bet, false, 0
);
if (sum > 21) {
activeGames.remove(userId);
return new BlackjackState(
game.playerCards().toArray(String[]::new),
game.dealerCards().toArray(String[]::new),
sum,
calculateSum(game.dealerCards()),
game.bet(),
true,
0);
}
public BlackjackState hit(String userId) {
ActiveBlackjack game = activeGames.get(userId);
if (game == null) return null;
return new BlackjackState(
game.playerCards().toArray(String[]::new),
game.dealerCards().toArray(String[]::new),
sum,
calculateSum(game.dealerCards()),
game.bet(),
false,
0);
}
game.playerCards().add(drawCard());
int sum = calculateSum(game.playerCards());
public BlackjackState stand(String userId) {
ActiveBlackjack game = activeGames.remove(userId);
if (game == null) return null;
if (sum > 21) {
activeGames.remove(userId);
return new BlackjackState(
game.playerCards().toArray(String[]::new), game.dealerCards().toArray(String[]::new),
sum, calculateSum(game.dealerCards()), game.bet(), true, 0
);
}
return new BlackjackState(
game.playerCards().toArray(String[]::new), game.dealerCards().toArray(String[]::new),
sum, calculateSum(game.dealerCards()), game.bet(), false, 0
);
List<String> dealerCards = new ArrayList<>(game.dealerCards());
while (calculateSum(dealerCards) < 16) {
dealerCards.add(drawCard());
}
public BlackjackState stand(String userId) {
ActiveBlackjack game = activeGames.remove(userId);
if (game == null) return null;
int playerSum = calculateSum(game.playerCards());
int dealerSum = calculateSum(dealerCards);
long payout;
List<String> dealerCards = new ArrayList<>(game.dealerCards());
while (calculateSum(dealerCards) < 16) {
dealerCards.add(drawCard());
}
int playerSum = calculateSum(game.playerCards());
int dealerSum = calculateSum(dealerCards);
long payout;
if (dealerSum > 21 || playerSum > dealerSum) {
payout = game.bet() * 2;
} else {
payout = 0;
}
EconomyAccount account = repository.getAccount(userId);
repository.save(account.withCash(account.getCash() + payout));
return new BlackjackState(
game.playerCards().toArray(String[]::new), dealerCards.toArray(String[]::new),
playerSum, dealerSum, game.bet(), true, payout
);
if (dealerSum > 21 || playerSum > dealerSum) {
payout = game.bet() * 2;
} else {
payout = 0;
}
private String drawCard() {
return DECK[ThreadLocalRandom.current().nextInt(DECK.length)];
}
EconomyAccount account = repository.getAccount(userId);
repository.save(account.withCash(account.getCash() + payout));
private int calculateSum(List<String> cards) {
int sum = 0;
int aces = 0;
for (String c : cards) {
switch (c) {
case "J", "Q", "K" -> sum += 10;
case "A" -> {
sum += 11; aces++;
}
default -> sum += Integer.parseInt(c);
}
return new BlackjackState(
game.playerCards().toArray(String[]::new),
dealerCards.toArray(String[]::new),
playerSum,
dealerSum,
game.bet(),
true,
payout);
}
private String drawCard() {
return DECK[ThreadLocalRandom.current().nextInt(DECK.length)];
}
private int calculateSum(List<String> cards) {
int sum = 0;
int aces = 0;
for (String c : cards) {
switch (c) {
case "J", "Q", "K" -> sum += 10;
case "A" -> {
sum += 11;
aces++;
}
while (sum > 21 && aces > 0) {
sum -= 10;
aces--;
}
return sum;
default -> sum += Integer.parseInt(c);
}
}
while (sum > 21 && aces > 0) {
sum -= 10;
aces--;
}
return sum;
}
public record BlackjackState(String[] playerCards, String[] dealerCards, int playerSum, int dealerSum, long bet, boolean isOver, long payout) {}
public record BlackjackResult(boolean playerWon, long payout, String reason) {}
public record BlackjackState(
String[] playerCards,
String[] dealerCards,
int playerSum,
int dealerSum,
long bet,
boolean isOver,
long payout) {}
public record BlackjackResult(boolean playerWon, long payout, String reason) {}
}

View file

@ -1,118 +1,123 @@
package creator.loki.feature.economy.service;
import creator.loki.core.security.CooldownManager;
import creator.loki.feature.economy.domain.EconomyAccount;
import creator.loki.feature.economy.repository.EconomyRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.LocalDate;
import java.util.List;
import creator.loki.core.security.CooldownManager;
import creator.loki.feature.economy.domain.EconomyAccount;
import creator.loki.feature.economy.domain.EconomyAccountBuilder;
import creator.loki.feature.economy.repository.EconomyRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class EconomyService {
private static final Logger logger = LoggerFactory.getLogger(EconomyService.class);
private static final Logger logger = LoggerFactory.getLogger(EconomyService.class);
private final EconomyRepository repository;
private final CooldownManager cooldownManager;
private final EconomyRepository repository;
private final CooldownManager cooldownManager;
public EconomyService(EconomyRepository repository, CooldownManager cooldownManager) {
this.repository = repository;
this.cooldownManager = cooldownManager;
public EconomyService(EconomyRepository repository, CooldownManager cooldownManager) {
this.repository = repository;
this.cooldownManager = cooldownManager;
}
public EconomyAccount getAccount(String userId) {
return repository.getAccount(userId);
}
public boolean transfer(String fromId, String toId, long amount) {
if (amount <= 0) return false;
EconomyAccount from = repository.getAccount(fromId);
if (from.getCash() < amount) return false;
EconomyAccount to = repository.getAccount(toId);
repository.save(from.withCash(from.getCash() - amount));
repository.save(to.withCash(to.getCash() + amount));
return true;
}
public boolean deposit(String userId, long amount) {
if (amount <= 0) return false;
EconomyAccount account = repository.getAccount(userId);
if (account.getCash() < amount) return false;
repository.save(
account.withCash(account.getCash() - amount).withBank(account.getBank() + amount));
return true;
}
public boolean withdraw(String userId, long amount) {
if (amount <= 0) return false;
EconomyAccount account = repository.getAccount(userId);
if (account.getBank() < amount) return false;
repository.save(
account.withBank(account.getBank() - amount).withCash(account.getCash() + amount));
return true;
}
public long earnCash(String userId, long amount) {
EconomyAccount account = repository.getAccount(userId);
EconomyAccount updated = account.withCash(account.getCash() + amount).withAddedEarned(amount);
repository.save(updated);
return updated.getCash();
}
public boolean spendCash(String userId, long amount) {
if (amount <= 0) return false;
EconomyAccount account = repository.getAccount(userId);
if (account.getCash() < amount) return false;
repository.save(account.withCash(account.getCash() - amount).withAddedSpent(amount));
return true;
}
public DailyResult claimDaily(String userId) {
EconomyAccount account = repository.getAccount(userId);
String today = LocalDate.now().toString();
if (today.equals(account.getLastDaily())) return null;
String yesterday = LocalDate.now().minusDays(1).toString();
int streak;
if (yesterday.equals(account.getLastDaily())) {
streak = account.getDailyStreak() + 1;
} else {
streak = 1;
}
public EconomyAccount getAccount(String userId) {
return repository.getAccount(userId);
}
public boolean transfer(String fromId, String toId, long amount) {
if (amount <= 0) return false;
EconomyAccount from = repository.getAccount(fromId);
if (from.getCash() < amount) return false;
EconomyAccount to = repository.getAccount(toId);
repository.save(from.withCash(from.getCash() - amount));
repository.save(to.withCash(to.getCash() + amount));
return true;
}
public boolean deposit(String userId, long amount) {
if (amount <= 0) return false;
EconomyAccount account = repository.getAccount(userId);
if (account.getCash() < amount) return false;
repository.save(account.withCash(account.getCash() - amount).withBank(account.getBank() + amount));
return true;
}
public boolean withdraw(String userId, long amount) {
if (amount <= 0) return false;
EconomyAccount account = repository.getAccount(userId);
if (account.getBank() < amount) return false;
repository.save(account.withBank(account.getBank() - amount).withCash(account.getCash() + amount));
return true;
}
public long earnCash(String userId, long amount) {
EconomyAccount account = repository.getAccount(userId);
EconomyAccount updated = account.withCash(account.getCash() + amount).withAddedEarned(amount);
repository.save(updated);
return updated.getCash();
}
public boolean spendCash(String userId, long amount) {
if (amount <= 0) return false;
EconomyAccount account = repository.getAccount(userId);
if (account.getCash() < amount) return false;
repository.save(account.withCash(account.getCash() - amount).withAddedSpent(amount));
return true;
}
public DailyResult claimDaily(String userId) {
EconomyAccount account = repository.getAccount(userId);
String today = LocalDate.now().toString();
if (today.equals(account.getLastDaily())) return null;
String yesterday = LocalDate.now().minusDays(1).toString();
int streak;
if (yesterday.equals(account.getLastDaily())) {
streak = account.getDailyStreak() + 1;
} else {
streak = 1;
}
long amount = streak >= 7 ? (long) (50 * 1.5) : 50;
long newCash = account.getCash() + amount;
EconomyAccount updated = new EconomyAccount.Builder(account)
long amount = streak >= 7 ? (long) (50 * 1.5) : 50;
long newCash = account.getCash() + amount;
EconomyAccount updated =
new EconomyAccountBuilder(account)
.cash(newCash)
.totalEarned(account.getTotalEarned() + amount)
.lastDaily(today)
.dailyStreak(streak)
.build();
repository.save(updated);
return new DailyResult(amount, streak, newCash);
}
repository.save(updated);
return new DailyResult(amount, streak, newCash);
}
public int upgradeFishingLuck(String userId) {
EconomyAccount account = repository.getAccount(userId);
if (account.getFishingLuck() >= 5) return -1;
long cost = 800L * (account.getFishingLuck() + 1);
if (account.getCash() < cost) return -1;
int newLevel = account.getFishingLuck() + 1;
repository.save(account.withCash(account.getCash() - cost).withFishingLuck(newLevel).withAddedSpent(cost));
return newLevel;
}
public int upgradeFishingLuck(String userId) {
EconomyAccount account = repository.getAccount(userId);
if (account.getFishingLuck() >= 5) return -1;
long cost = 800L * (account.getFishingLuck() + 1);
if (account.getCash() < cost) return -1;
int newLevel = account.getFishingLuck() + 1;
repository.save(
account.withCash(account.getCash() - cost).withFishingLuck(newLevel).withAddedSpent(cost));
return newLevel;
}
public int upgradeEfficiency(String userId) {
EconomyAccount account = repository.getAccount(userId);
if (account.getEfficiency() >= 5) return -1;
long cost = 800L * (account.getEfficiency() + 1);
if (account.getCash() < cost) return -1;
int newLevel = account.getEfficiency() + 1;
repository.save(account.withCash(account.getCash() - cost).withEfficiency(newLevel).withAddedSpent(cost));
return newLevel;
}
public int upgradeEfficiency(String userId) {
EconomyAccount account = repository.getAccount(userId);
if (account.getEfficiency() >= 5) return -1;
long cost = 800L * (account.getEfficiency() + 1);
if (account.getCash() < cost) return -1;
int newLevel = account.getEfficiency() + 1;
repository.save(
account.withCash(account.getCash() - cost).withEfficiency(newLevel).withAddedSpent(cost));
return newLevel;
}
public List<EconomyAccount> getTopBalances(int limit) {
return repository.findTopByBalance(limit);
}
public record DailyResult(long amount, int streak, long newCash) {}
public List<EconomyAccount> getTopBalances(int limit) {
return repository.findTopByBalance(limit);
}
public record DailyResult(long amount, int streak, long newCash) {}
}

View file

@ -1,136 +1,157 @@
package creator.loki.feature.economy.service;
import creator.loki.feature.economy.domain.EconomyAccount;
import creator.loki.feature.economy.repository.EconomyRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
import creator.loki.feature.economy.domain.EconomyAccount;
import creator.loki.feature.economy.domain.EconomyAccountBuilder;
import creator.loki.feature.economy.repository.EconomyRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class FishingService {
private static final Logger logger = LoggerFactory.getLogger(FishingService.class);
private static final Logger logger = LoggerFactory.getLogger(FishingService.class);
private static final String[] FISH = {"\uD83D\uDC1F", "\uD83D\uDC20", "\uD83D\uDC21", "\uD83D\uDC19"};
private static final String WAVE = "\uD83C\uDF0A";
private static final long COOLDOWN_MS = 7200000;
private static final String[] FISH = {
"\uD83D\uDC1F", "\uD83D\uDC20", "\uD83D\uDC21", "\uD83D\uDC19"
};
private static final String WAVE = "\uD83C\uDF0A";
private static final long COOLDOWN_MS = 7200000;
private final EconomyRepository repository;
private final ConcurrentHashMap<String, ActiveFish> activeFishing = new ConcurrentHashMap<>();
private final EconomyRepository repository;
private final ConcurrentHashMap<String, ActiveFish> activeFishing = new ConcurrentHashMap<>();
public FishingService(EconomyRepository repository) {
this.repository = repository;
public FishingService(EconomyRepository repository) {
this.repository = repository;
}
private record ActiveFish(
String targetFish, int correctCount, long startTime, String difficulty, String[] grid) {}
public FishResult startFishing(String userId, String difficulty) {
EconomyAccount account = repository.getAccount(userId);
if (account.getLastFish() != null) {
long elapsed = System.currentTimeMillis() - Long.parseLong(account.getLastFish());
if (elapsed < COOLDOWN_MS) return null;
}
private record ActiveFish(String targetFish, int correctCount, long startTime, String difficulty, String[] grid) {}
public FishResult startFishing(String userId, String difficulty) {
EconomyAccount account = repository.getAccount(userId);
if (account.getLastFish() != null) {
long elapsed = System.currentTimeMillis() - Long.parseLong(account.getLastFish());
if (elapsed < COOLDOWN_MS) return null;
}
int size;
int fishTypes;
int baseReward;
long timeLimit;
switch (difficulty.toLowerCase()) {
case "easy" -> {
size = 5; fishTypes = 2; baseReward = 15; timeLimit = 30000;
}
case "normal" -> {
size = 7; fishTypes = 3; baseReward = 30; timeLimit = 45000;
}
case "hard" -> {
size = 9; fishTypes = 4; baseReward = 50; timeLimit = 60000;
}
default -> {
return null;
}
}
String[] available = Arrays.copyOf(FISH, fishTypes);
String[][] grid = new String[size][size];
int[] counts = new int[fishTypes];
var rng = ThreadLocalRandom.current();
for (int r = 0; r < size; r++) {
for (int c = 0; c < size; c++) {
if (rng.nextInt(100) < 40) {
grid[r][c] = WAVE;
} else {
int fi = rng.nextInt(fishTypes);
grid[r][c] = available[fi];
counts[fi]++;
}
}
}
int targetIdx;
do {
targetIdx = rng.nextInt(fishTypes);
} while (counts[targetIdx] == 0);
String targetFish = available[targetIdx];
int correctCount = counts[targetIdx];
String[] rows = new String[size];
for (int r = 0; r < size; r++) {
rows[r] = String.join(" ", grid[r]);
}
ActiveFish session = new ActiveFish(targetFish, correctCount, System.currentTimeMillis(), difficulty, rows);
activeFishing.put(userId, session);
return new FishResult(rows, targetFish, correctCount, 0, account.getCash());
int size;
int fishTypes;
int baseReward;
switch (difficulty.toLowerCase()) {
case "easy" -> {
size = 5;
fishTypes = 2;
baseReward = 15;
}
case "normal" -> {
size = 7;
fishTypes = 3;
baseReward = 30;
}
case "hard" -> {
size = 9;
fishTypes = 4;
baseReward = 50;
}
default -> {
return null;
}
}
public FishResult checkAnswer(String userId, int answer, long startTime, String difficulty) {
ActiveFish session = activeFishing.remove(userId);
if (session == null) return null;
if (session.startTime() != startTime || !session.difficulty().equals(difficulty)) return null;
String[] available = Arrays.copyOf(FISH, fishTypes);
String[][] grid = new String[size][size];
int[] counts = new int[fishTypes];
var rng = ThreadLocalRandom.current();
EconomyAccount account = repository.getAccount(userId);
long elapsed = System.currentTimeMillis() - session.startTime();
int baseReward;
long timeLimit;
switch (difficulty.toLowerCase()) {
case "easy" -> {
baseReward = 15; timeLimit = 30000;
}
case "normal" -> {
baseReward = 30; timeLimit = 45000;
}
case "hard" -> {
baseReward = 50; timeLimit = 60000;
}
default -> {
return null;
}
for (int r = 0; r < size; r++) {
for (int c = 0; c < size; c++) {
if (rng.nextInt(100) < 40) {
grid[r][c] = WAVE;
} else {
int fi = rng.nextInt(fishTypes);
grid[r][c] = available[fi];
counts[fi]++;
}
}
}
long reward = 0;
if (answer == session.correctCount() && elapsed <= timeLimit) {
double ratio = (double) elapsed / timeLimit;
double speedMult = ratio < 0.3 ? 2.0 : ratio < 0.5 ? 1.5 : 1.0;
reward = (long) (baseReward * speedMult * (1 + account.getFishingLuck() * 0.01));
}
int targetIdx;
do {
targetIdx = rng.nextInt(fishTypes);
} while (counts[targetIdx] == 0);
long newCash = account.getCash() + reward;
EconomyAccount updated = new EconomyAccount.Builder(account)
String targetFish = available[targetIdx];
int correctCount = counts[targetIdx];
String[] rows = new String[size];
for (int r = 0; r < size; r++) {
rows[r] = String.join(" ", grid[r]);
}
ActiveFish session =
new ActiveFish(targetFish, correctCount, System.currentTimeMillis(), difficulty, rows);
activeFishing.put(userId, session);
return new FishResult(rows, targetFish, correctCount, 0, account.getCash());
}
public FishResult checkAnswer(String userId, int answer) {
ActiveFish session = activeFishing.get(userId);
if (session == null) return null;
return checkAnswer(userId, answer, session.startTime(), session.difficulty());
}
public FishResult checkAnswer(String userId, int answer, long startTime, String difficulty) {
ActiveFish session = activeFishing.remove(userId);
if (session == null) return null;
if (session.startTime() != startTime || !session.difficulty().equals(difficulty)) return null;
EconomyAccount account = repository.getAccount(userId);
long elapsed = System.currentTimeMillis() - session.startTime();
int baseReward;
long timeLimit;
switch (difficulty.toLowerCase()) {
case "easy" -> {
baseReward = 15;
timeLimit = 30000;
}
case "normal" -> {
baseReward = 30;
timeLimit = 45000;
}
case "hard" -> {
baseReward = 50;
timeLimit = 60000;
}
default -> {
return null;
}
}
long reward = 0;
if (answer == session.correctCount() && elapsed <= timeLimit) {
double ratio = (double) elapsed / timeLimit;
double speedMult = ratio < 0.3 ? 2.0 : ratio < 0.5 ? 1.5 : 1.0;
reward = (long) (baseReward * speedMult * (1 + account.getFishingLuck() * 0.01));
}
long newCash = account.getCash() + reward;
EconomyAccount updated =
new EconomyAccountBuilder(account)
.cash(newCash)
.totalEarned(account.getTotalEarned() + reward)
.lastFish(String.valueOf(System.currentTimeMillis()))
.build();
repository.save(updated);
repository.save(updated);
return new FishResult(session.grid(), session.targetFish(), session.correctCount(), reward, newCash);
}
public record FishResult(String[] grid, String targetFish, int count, long reward, long cashAfter) {}
return new FishResult(
session.grid(), session.targetFish(), session.correctCount(), reward, newCash);
}
public record FishResult(
String[] grid, String targetFish, int count, long reward, long cashAfter) {}
}

View file

@ -1,91 +1,96 @@
package creator.loki.feature.economy.service;
import creator.loki.feature.economy.domain.EconomyAccount;
import creator.loki.feature.economy.domain.Job;
import creator.loki.feature.economy.repository.EconomyRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import creator.loki.feature.economy.domain.EconomyAccount;
import creator.loki.feature.economy.domain.EconomyAccountBuilder;
import creator.loki.feature.economy.domain.Job;
import creator.loki.feature.economy.repository.EconomyRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class JobService {
private static final Logger logger = LoggerFactory.getLogger(JobService.class);
private static final Logger logger = LoggerFactory.getLogger(JobService.class);
private static final long WORK_COOLDOWN = 21600000;
private static final long WORK_COOLDOWN = 21600000;
private final EconomyRepository repository;
private final EconomyRepository repository;
public JobService(EconomyRepository repository) {
this.repository = repository;
public JobService(EconomyRepository repository) {
this.repository = repository;
}
public void setJob(String userId, String jobName) {
Job job = Job.fromName(jobName);
if (job == null) return;
EconomyAccount account = repository.getAccount(userId);
EconomyAccount updated = new EconomyAccountBuilder(account).job(job.name()).build();
repository.save(updated);
}
public WorkResult work(String userId, int userLevel) {
EconomyAccount account = repository.getAccount(userId);
String jobName = account.getJob();
if (jobName == null) return null;
Job job = Job.fromName(jobName);
if (job == null) return null;
if (userLevel < job.getRequiredLevel()) return null;
if (account.getLastWork() != null) {
long elapsed = System.currentTimeMillis() - Long.parseLong(account.getLastWork());
if (elapsed < WORK_COOLDOWN) return null;
}
public void setJob(String userId, String jobName) {
Job job = Job.fromName(jobName);
if (job == null) return;
EconomyAccount account = repository.getAccount(userId);
EconomyAccount updated = new EconomyAccount.Builder(account)
.job(job.name())
.build();
repository.save(updated);
}
var rng = ThreadLocalRandom.current();
long basePayout = rng.nextLong(job.getMinPayout(), job.getMaxPayout() + 1);
long amount = (long) (basePayout * (1 + account.getEfficiency() * 0.01));
public WorkResult work(String userId, int userLevel) {
EconomyAccount account = repository.getAccount(userId);
String jobName = account.getJob();
if (jobName == null) return null;
Job job = Job.fromName(jobName);
if (job == null) return null;
if (userLevel < job.getRequiredLevel()) return null;
if (account.getLastWork() != null) {
long elapsed = System.currentTimeMillis() - Long.parseLong(account.getLastWork());
if (elapsed < WORK_COOLDOWN) return null;
}
var rng = ThreadLocalRandom.current();
long basePayout = rng.nextLong(job.getMinPayout(), job.getMaxPayout() + 1);
long amount = (long) (basePayout * (1 + account.getEfficiency() * 0.01));
long newCash = account.getCash() + amount;
EconomyAccount updated = new EconomyAccount.Builder(account)
long newCash = account.getCash() + amount;
EconomyAccount updated =
new EconomyAccountBuilder(account)
.cash(newCash)
.totalEarned(account.getTotalEarned() + amount)
.lastWork(String.valueOf(System.currentTimeMillis()))
.build();
repository.save(updated);
repository.save(updated);
return new WorkResult(amount, newCash);
return new WorkResult(amount, newCash);
}
public List<JobInfo> listJobs() {
List<JobInfo> infos = new ArrayList<>();
for (Job job : Job.values()) {
infos.add(
new JobInfo(
job.name(),
job.getDisplayName(),
job.getRequiredLevel(),
job.getMinPayout(),
job.getMaxPayout(),
job.getEmoji()));
}
return infos;
}
public List<JobInfo> listJobs() {
List<JobInfo> infos = new ArrayList<>();
for (Job job : Job.values()) {
infos.add(new JobInfo(
job.name(),
job.getDisplayName(),
job.getRequiredLevel(),
job.getMinPayout(),
job.getMaxPayout(),
job.getEmoji()
));
}
return infos;
}
public String getCurrentJob(String userId) {
EconomyAccount account = repository.getAccount(userId);
String jobName = account.getJob();
if (jobName == null) return null;
Job job = Job.fromName(jobName);
return job != null ? job.getDisplayName() : null;
}
public String getCurrentJob(String userId) {
EconomyAccount account = repository.getAccount(userId);
String jobName = account.getJob();
if (jobName == null) return null;
Job job = Job.fromName(jobName);
return job != null ? job.getDisplayName() : null;
}
public record WorkResult(long amount, long newCash) {}
public record JobInfo(String name, String displayName, int requiredLevel, int minPayout, int maxPayout, String emoji) {}
public record WorkResult(long amount, long newCash) {}
public record JobInfo(
String name,
String displayName,
int requiredLevel,
int minPayout,
int maxPayout,
String emoji) {}
}

View file

@ -1,60 +1,72 @@
package creator.loki.feature.economy.service;
import java.util.concurrent.ThreadLocalRandom;
import creator.loki.feature.economy.domain.EconomyAccount;
import creator.loki.feature.economy.domain.SlotCombination;
import creator.loki.feature.economy.repository.EconomyRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.ThreadLocalRandom;
public class SlotService {
private static final Logger logger = LoggerFactory.getLogger(SlotService.class);
private static final Logger logger = LoggerFactory.getLogger(SlotService.class);
private static final String[] EMOJIS = {"\uD83C\uDF52", "\uD83D\uDD14", "\uD83D\uDC8E", "\uD83D\uDC51", "\u26A1"};
private static final int[] WEIGHTS = {40, 25, 18, 12, 5};
private static final int TOTAL_WEIGHT = 100;
private static final String[] EMOJIS = {
"\uD83C\uDF52", "\uD83D\uDD14", "\uD83D\uDC8E", "\uD83D\uDC51", "\u26A1"
};
private static final int[] WEIGHTS = {40, 25, 18, 12, 5};
private static final int TOTAL_WEIGHT = 100;
private final EconomyRepository repository;
private final EconomyRepository repository;
public SlotService(EconomyRepository repository) {
this.repository = repository;
public SlotService(EconomyRepository repository) {
this.repository = repository;
}
public SlotResult spin(String userId, long bet) {
if (bet <= 0) return null;
EconomyAccount account = repository.getAccount(userId);
if (account.getCash() < bet) return null;
repository.save(account.withCash(account.getCash() - bet));
String e1 = spinEmoji();
String e2 = spinEmoji();
String e3 = spinEmoji();
SlotCombination combo = SlotCombination.match(e1, e2, e3);
long winAmount = combo != null ? bet * combo.getMultiplier() : 0;
long cashAfter = account.getCash() - bet + winAmount;
repository.save(repository.getAccount(userId).withCash(cashAfter).withAddedEarned(winAmount));
return new SlotResult(
e1,
e2,
e3,
combo != null ? combo.name() : null,
combo != null ? combo.getMultiplier() : 0,
winAmount,
cashAfter);
}
private String spinEmoji() {
int roll = ThreadLocalRandom.current().nextInt(TOTAL_WEIGHT);
int cumulative = 0;
for (int i = 0; i < EMOJIS.length; i++) {
cumulative += WEIGHTS[i];
if (roll < cumulative) return EMOJIS[i];
}
return EMOJIS[EMOJIS.length - 1];
}
public SlotResult spin(String userId, long bet) {
if (bet <= 0) return null;
EconomyAccount account = repository.getAccount(userId);
if (account.getCash() < bet) return null;
repository.save(account.withCash(account.getCash() - bet));
String e1 = spinEmoji();
String e2 = spinEmoji();
String e3 = spinEmoji();
SlotCombination combo = SlotCombination.match(e1, e2, e3);
long winAmount = combo != null ? bet * combo.getMultiplier() : 0;
long cashAfter = account.getCash() - bet + winAmount;
repository.save(repository.getAccount(userId).withCash(cashAfter).withAddedEarned(winAmount));
return new SlotResult(e1, e2, e3,
combo != null ? combo.name() : null,
combo != null ? combo.getMultiplier() : 0,
winAmount, cashAfter);
}
private String spinEmoji() {
int roll = ThreadLocalRandom.current().nextInt(TOTAL_WEIGHT);
int cumulative = 0;
for (int i = 0; i < EMOJIS.length; i++) {
cumulative += WEIGHTS[i];
if (roll < cumulative) return EMOJIS[i];
}
return EMOJIS[EMOJIS.length - 1];
}
public record SlotResult(String emoji1, String emoji2, String emoji3, String comboName, int multiplier, long winAmount, long cashAfter) {}
public record SlotResult(
String emoji1,
String emoji2,
String emoji3,
String comboName,
int multiplier,
long winAmount,
long cashAfter) {}
}

View file

@ -1,64 +1,60 @@
package creator.loki.feature.entertainment;
import com.google.inject.Inject;
import creator.loki.feature.social.commands.*;
import creator.loki.feature.social.commands.*;
import creator.loki.feature.entertainment.commands.*;
import creator.loki.feature.entertainment.commands.*;
import creator.loki.config.BotConfig;
import creator.loki.core.registry.CommandRegistrar;
import creator.loki.core.registry.CommandRegistry;
import creator.loki.core.security.CooldownManager;
import creator.loki.feature.ship.repository.ShipPairRepository;
import creator.loki.feature.entertainment.commands.*;
import creator.loki.feature.ship.ShipCommand;
import creator.loki.feature.ship.repository.ShipPairRepository;
import creator.loki.feature.social.commands.*;
public class EntertainmentCommandRegistrar implements CommandRegistrar {
private final CooldownManager cooldownManager;
private final ShipPairRepository shipPairRepository;
private final BotConfig config;
private final CooldownManager cooldownManager;
private final ShipPairRepository shipPairRepository;
private final BotConfig config;
@Inject
public EntertainmentCommandRegistrar(
CooldownManager cooldownManager,
ShipPairRepository shipPairRepository,
BotConfig config
) {
this.cooldownManager = cooldownManager;
this.shipPairRepository = shipPairRepository;
this.config = config;
}
@Inject
public EntertainmentCommandRegistrar(
CooldownManager cooldownManager, ShipPairRepository shipPairRepository, BotConfig config) {
this.cooldownManager = cooldownManager;
this.shipPairRepository = shipPairRepository;
this.config = config;
}
@Override
public void register(CommandRegistry registry) {
registry.register(new EightBallCommand());
registry.register(new CoinCommand());
registry.register(new DiceCommand());
registry.register(new MemeCommand());
registry.register(new ShipCommand(shipPairRepository));
registry.register(new RoastCommand());
registry.register(new ComplimentCommand());
registry.register(new GlazbobaCommand());
@Override
public void register(CommandRegistry registry) {
registry.register(new EightBallCommand());
registry.register(new CoinCommand());
registry.register(new DiceCommand());
registry.register(new MemeCommand());
registry.register(new ShipCommand(shipPairRepository));
registry.register(new RoastCommand());
registry.register(new ComplimentCommand());
registry.register(new GlazbobaCommand());
registry.register(new IqCommand(cooldownManager));
registry.register(new GayCommand(cooldownManager));
registry.register(new PpCommand(cooldownManager));
registry.register(new RateCommand(cooldownManager));
registry.register(new BoykisserCommand(cooldownManager));
registry.register(new MustacheCommand(cooldownManager));
registry.register(new FatCommand(cooldownManager));
registry.register(new IqCommand(cooldownManager));
registry.register(new GayCommand(cooldownManager));
registry.register(new PpCommand(cooldownManager));
registry.register(new RateCommand(cooldownManager));
registry.register(new BoykisserCommand(cooldownManager));
registry.register(new MustacheCommand(cooldownManager));
registry.register(new FatCommand(cooldownManager));
registry.register(new PatCommand(cooldownManager));
registry.register(new HugCommand());
registry.register(new KissCommand(cooldownManager));
registry.register(new SlapCommand(cooldownManager));
registry.register(new TickleCommand(cooldownManager));
registry.register(new PokeCommand(cooldownManager));
registry.register(new CuddleCommand(cooldownManager));
registry.register(new FeedCommand(cooldownManager));
registry.register(new PatCommand(cooldownManager));
registry.register(new HugCommand());
registry.register(new KissCommand(cooldownManager));
registry.register(new SlapCommand(cooldownManager));
registry.register(new TickleCommand(cooldownManager));
registry.register(new PokeCommand(cooldownManager));
registry.register(new CuddleCommand(cooldownManager));
registry.register(new FeedCommand(cooldownManager));
registry.register(new RandomChoiceCommand(config, "choose", RandomChoiceCommand.Flavor.DEFAULT));
registry.register(new RandomChoiceCommand(config, "pick", RandomChoiceCommand.Flavor.DEFAULT));
registry.register(new RandomChoiceCommand(config, "decide", RandomChoiceCommand.Flavor.ORACLE));
registry.register(new RandomChoiceCommand(config, "wheel", RandomChoiceCommand.Flavor.WHEEL));
}
registry.register(
new RandomChoiceCommand(config, "choose", RandomChoiceCommand.Flavor.DEFAULT));
registry.register(new RandomChoiceCommand(config, "pick", RandomChoiceCommand.Flavor.DEFAULT));
registry.register(new RandomChoiceCommand(config, "decide", RandomChoiceCommand.Flavor.ORACLE));
registry.register(new RandomChoiceCommand(config, "wheel", RandomChoiceCommand.Flavor.WHEEL));
}
}

View file

@ -1,101 +1,104 @@
package creator.loki.feature.entertainment.commands;
import creator.loki.feature.entertainment.commands.RatingConfig;
import creator.loki.feature.entertainment.commands.RatingLevel;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import creator.loki.core.security.CooldownManager;
import net.dv8tion.jda.api.entities.Member;
import java.awt.Color;
import java.util.List;
import java.util.Random;
/**
* Boykisser метр с детерминированным результатом
*/
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.core.security.CooldownManager;
import creator.loki.embed.EmbedFactory;
import net.dv8tion.jda.api.entities.Member;
/** Boykisser метр с детерминированным результатом */
public class BoykisserCommand implements Command {
private final CooldownManager cooldownManager;
private final CooldownManager cooldownManager;
public BoykisserCommand(CooldownManager cooldownManager) {
this.cooldownManager = cooldownManager;
public BoykisserCommand(CooldownManager cooldownManager) {
this.cooldownManager = cooldownManager;
}
@Override
public String getName() {
return "boykisser";
}
@Override
public String getDescription() {
return "На сколько ты boykisser (результат всегда одинаковый)";
}
@Override
public String getUsage() {
return "boykisser [@user]";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
// Проверка cooldown (5 секунд)
if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "boykisser")) {
long remaining =
cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "boykisser");
context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue();
return;
}
@Override
public String getName() {
return "boykisser";
List<Member> mentioned = context.getMessage().getMentions().getMembers();
Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0);
if (target == null) return;
if (target.getUser().isBot()) {
context.getChannel().sendMessage("❌ Боты не целуют мальчиков!").queue();
return;
}
@Override
public String getDescription() {
return "На сколько ты boykisser (результат всегда одинаковый)";
// Применяем cooldown
cooldownManager.applyCooldown(context.getAuthor().getId(), "boykisser", 5);
// Детерминированный процент
int boykisserLevel = getDeterministicValue(target.getId(), 777, 0, 100);
// Прогресс-бар с поцелуями
StringBuilder bar = new StringBuilder();
int filled = boykisserLevel / 10;
for (int i = 0; i < filled; i++) {
bar.append("💋");
}
for (int i = filled; i < 10; i++) {
bar.append("⬜");
}
@Override
public String getUsage() {
return "boykisser [@user]";
}
// Определяем уровень
RatingLevel level = RatingLevel.findLevel(boykisserLevel, RatingConfig.getBoykisserLevels());
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
// Проверка cooldown (5 секунд)
if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "boykisser")) {
long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "boykisser");
context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue();
return;
}
List<Member> mentioned = context.getMessage().getMentions().getMembers();
Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0);
if (target == null) return;
if (target.getUser().isBot()) {
context.getChannel().sendMessage("❌ Боты не целуют мальчиков!").queue();
return;
}
// Применяем cooldown
cooldownManager.applyCooldown(context.getAuthor().getId(), "boykisser", 5);
// Детерминированный процент
int boykisserLevel = getDeterministicValue(target.getId(), 777, 0, 100);
// Прогресс-бар с поцелуями
StringBuilder bar = new StringBuilder();
int filled = boykisserLevel / 10;
for (int i = 0; i < filled; i++) {
bar.append("💋");
}
for (int i = filled; i < 10; i++) {
bar.append("⬜");
}
// Определяем уровень
RatingLevel level = RatingLevel.findLevel(boykisserLevel, RatingConfig.getBoykisserLevels());
context.getChannel().sendMessageEmbeds(
context
.getChannel()
.sendMessageEmbeds(
EmbedFactory.create()
.withTitle("💋 Boykisser Метр")
.withDescription(target.getAsMention())
.addField("Уровень", bar.toString() + "\n**" + boykisserLevel + "%** - " + level.getComment(), false)
.addField(
"Уровень",
bar.toString() + "\n**" + boykisserLevel + "%** - " + level.getComment(),
false)
.withThumbnail(target.getUser().getAvatarUrl())
.withColor(level.getColor())
.withFooter("💡 Детерминировано вашим Discord ID")
.build()
).queue();
}
.build())
.queue();
}
private int getDeterministicValue(String userId, int salt, int min, int max) {
long seed = Long.parseLong(userId) + salt;
Random random = new Random(seed);
return random.nextInt(max - min + 1) + min;
}
private static final Random RANDOM = new Random();
private int getDeterministicValue(String userId, int salt, int min, int max) {
long seed = Long.parseLong(userId) + salt;
RANDOM.setSeed(seed);
return RANDOM.nextInt(max - min + 1) + min;
}
}

View file

@ -10,69 +10,64 @@ import java.util.function.Function;
import java.util.function.Predicate;
import java.util.regex.Pattern;
/**
* Разбор хвоста сообщения и строки вариантов для {@link RandomChoiceCommand}.
*/
/** Разбор хвоста сообщения и строки вариантов для {@link RandomChoiceCommand}. */
final class ChoiceOptionParser {
private static final Pattern OR_SPLIT = Pattern.compile("(?i)\\s+или\\s+");
private static final Pattern OR_SPLIT = Pattern.compile("(?i)\\s+или\\s+");
private record SplitRule(Predicate<String> when, Function<String, String[]> split) {
private record SplitRule(Predicate<String> when, Function<String, String[]> split) {}
/** Порядок важен: последнее правило - «слова через пробел». */
private static final List<SplitRule> RULES =
List.of(
new SplitRule(s -> s.contains("|"), s -> s.split("\\|", -1)),
new SplitRule(s -> s.contains(","), s -> s.split(",", -1)),
new SplitRule(s -> OR_SPLIT.matcher(s).find(), OR_SPLIT::split),
new SplitRule(s -> s.contains("/"), s -> s.split("/", -1)),
new SplitRule(s -> true, s -> s.split("\\s+", -1)));
private ChoiceOptionParser() {}
static String tailAfterCommand(String raw, String prefix, String commandName) {
String t = raw.trim();
if (!t.startsWith(prefix)) {
return "";
}
/** Порядок важен: последнее правило - «слова через пробел». */
private static final List<SplitRule> RULES = List.of(
new SplitRule(s -> s.contains("|"), s -> s.split("\\|", -1)),
new SplitRule(s -> s.contains(","), s -> s.split(",", -1)),
new SplitRule(s -> OR_SPLIT.matcher(s).find(), OR_SPLIT::split),
new SplitRule(s -> s.contains("/"), s -> s.split("/", -1)),
new SplitRule(s -> true, s -> s.split("\\s+", -1))
);
private ChoiceOptionParser() {
String rest = t.substring(prefix.length()).trim();
int sp = rest.indexOf(' ');
if (sp < 0) {
return "";
}
static String tailAfterCommand(String raw, String prefix, String commandName) {
String t = raw.trim();
if (!t.startsWith(prefix)) {
return "";
}
String rest = t.substring(prefix.length()).trim();
int sp = rest.indexOf(' ');
if (sp < 0) {
return "";
}
String cmd = rest.substring(0, sp);
if (!cmd.equalsIgnoreCase(commandName)) {
return "";
}
return rest.substring(sp).trim();
String cmd = rest.substring(0, sp);
if (!cmd.equalsIgnoreCase(commandName)) {
return "";
}
return rest.substring(sp).trim();
}
static List<String> parseOptions(String tail) {
String t = tail.strip();
for (SplitRule rule : RULES) {
if (rule.when.test(t)) {
return dedupeIgnoreCase(trimNonEmpty(rule.split.apply(t)));
}
}
return List.of();
static List<String> parseOptions(String tail) {
String t = tail.strip();
for (SplitRule rule : RULES) {
if (rule.when.test(t)) {
return dedupeIgnoreCase(trimNonEmpty(rule.split.apply(t)));
}
}
return List.of();
}
private static List<String> trimNonEmpty(String[] arr) {
return Arrays.stream(arr).map(String::trim).filter(s -> !s.isEmpty()).toList();
}
private static List<String> trimNonEmpty(String[] arr) {
return Arrays.stream(arr).map(String::trim).filter(s -> !s.isEmpty()).toList();
}
private static List<String> dedupeIgnoreCase(List<String> parts) {
Set<String> seen = new HashSet<>();
List<String> out = new ArrayList<>();
for (String p : parts) {
String key = p.toLowerCase(Locale.ROOT);
if (seen.add(key)) {
out.add(p);
}
}
return List.copyOf(out);
private static List<String> dedupeIgnoreCase(List<String> parts) {
Set<String> seen = new HashSet<>();
List<String> out = new ArrayList<>();
for (String p : parts) {
String key = p.toLowerCase(Locale.ROOT);
if (seen.add(key)) {
out.add(p);
}
}
return List.copyOf(out);
}
}

View file

@ -1,47 +1,49 @@
package creator.loki.feature.entertainment.commands;
import java.awt.Color;
import java.util.Random;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import java.awt.Color;
import java.util.Random;
public class CoinCommand implements Command {
private final Random random = new Random();
private final Random random = new Random();
@Override
public String getName() {
return "coin";
}
@Override
public String getName() {
return "coin";
}
@Override
public String getDescription() {
return "Подбросить монетку";
}
@Override
public String getDescription() {
return "Подбросить монетку";
}
@Override
public String getUsage() {
return "coin";
}
@Override
public String getUsage() {
return "coin";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
boolean isHeads = random.nextBoolean();
String result = isHeads ? "Орёл" : "Решка";
String emoji = isHeads ? "🦅" : "🪙";
@Override
public void execute(CommandContext context) {
boolean isHeads = random.nextBoolean();
String result = isHeads ? "Орёл" : "Решка";
String emoji = isHeads ? "🦅" : "🪙";
context.getChannel().sendMessageEmbeds(
context
.getChannel()
.sendMessageEmbeds(
EmbedFactory.create()
.withTitle(emoji + " Подбрасываю монетку...")
.withDescription("**Результат:** " + result)
.withColor(isHeads ? new Color(255, 215, 0) : new Color(192, 192, 192))
.build()
).queue();
}
.build())
.queue();
}
}

View file

@ -1,60 +1,62 @@
package creator.loki.feature.entertainment.commands;
import java.awt.Color;
import java.util.Random;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import java.awt.Color;
import java.util.Random;
public class DiceCommand implements Command {
private final Random random = new Random();
private final Random random = new Random();
@Override
public String getName() {
return "dice";
}
@Override
public String getName() {
return "dice";
}
@Override
public String getDescription() {
return "Бросить кубик";
}
@Override
public String getDescription() {
return "Бросить кубик";
}
@Override
public String getUsage() {
return "dice [N]";
}
@Override
public String getUsage() {
return "dice [N]";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
int maxValue = 6;
@Override
public void execute(CommandContext context) {
int maxValue = 6;
if (!context.getArgs().isEmpty()) {
try {
maxValue = Integer.parseInt(context.getArgs().get(0));
if (maxValue < 2 || maxValue > 100) {
context.getChannel().sendMessage("Число должно быть от 2 до 100").queue();
return;
}
} catch (NumberFormatException e) {
context.getChannel().sendMessage("Укажи корректное число").queue();
return;
}
if (!context.getArgs().isEmpty()) {
try {
maxValue = Integer.parseInt(context.getArgs().get(0));
if (maxValue < 2 || maxValue > 100) {
context.getChannel().sendMessage("Число должно быть от 2 до 100").queue();
return;
}
} catch (NumberFormatException e) {
context.getChannel().sendMessage("Укажи корректное число").queue();
return;
}
}
int result = random.nextInt(maxValue) + 1;
int result = random.nextInt(maxValue) + 1;
context.getChannel().sendMessageEmbeds(
context
.getChannel()
.sendMessageEmbeds(
EmbedFactory.create()
.withTitle("🎲 Бросаю кубик...")
.withDescription("**Результат:** " + result + " (из " + maxValue + ")")
.withColor(new Color(46, 204, 113))
.build()
).queue();
}
.build())
.queue();
}
}

View file

@ -1,74 +1,76 @@
package creator.loki.feature.entertainment.commands;
import java.awt.Color;
import java.util.Random;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import java.awt.Color;
import java.util.Random;
public class EightBallCommand implements Command {
private final Random random = new Random();
private final Random random = new Random();
private static final String[] ANSWERS = {
"Да, определённо",
"Бесспорно",
"Без сомнений",
"Можешь быть уверен",
"Мне кажется - да",
"Вероятнее всего",
"Хорошие перспективы",
"Знаки говорят - да",
"Да",
"Пока не ясно, попробуй снова",
"Спроси позже",
"Лучше не рассказывать",
"Сейчас нельзя предсказать",
"Сконцентрируйся и спроси опять",
"Даже не думай",
"Мой ответ - нет",
"По моим данным - нет",
"Перспективы не очень хорошие",
"Весьма сомнительно"
};
private static final String[] ANSWERS = {
"Да, определённо",
"Бесспорно",
"Без сомнений",
"Можешь быть уверен",
"Мне кажется - да",
"Вероятнее всего",
"Хорошие перспективы",
"Знаки говорят - да",
"Да",
"Пока не ясно, попробуй снова",
"Спроси позже",
"Лучше не рассказывать",
"Сейчас нельзя предсказать",
"Сконцентрируйся и спроси опять",
"Даже не думай",
"Мой ответ - нет",
"По моим данным - нет",
"Перспективы не очень хорошие",
"Весьма сомнительно"
};
@Override
public String getName() {
return "8ball";
@Override
public String getName() {
return "8ball";
}
@Override
public String getDescription() {
return "Магический шар предсказаний";
}
@Override
public String getUsage() {
return "8ball <вопрос>";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
if (context.getArgs().isEmpty()) {
context.getChannel().sendMessage("Задай вопрос!").queue();
return;
}
@Override
public String getDescription() {
return "Магический шар предсказаний";
}
String question = String.join(" ", context.getArgs());
String answer = ANSWERS[random.nextInt(ANSWERS.length)];
@Override
public String getUsage() {
return "8ball <вопрос>";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
if (context.getArgs().isEmpty()) {
context.getChannel().sendMessage("Задай вопрос!").queue();
return;
}
String question = String.join(" ", context.getArgs());
String answer = ANSWERS[random.nextInt(ANSWERS.length)];
context.getChannel().sendMessageEmbeds(
context
.getChannel()
.sendMessageEmbeds(
EmbedFactory.create()
.withTitle("🎱 Магический шар")
.addField("Вопрос:", question, false)
.addField("Ответ:", answer, false)
.withColor(new Color(52, 73, 94))
.build()
).queue();
}
.build())
.queue();
}
}

View file

@ -1,96 +1,98 @@
package creator.loki.feature.entertainment.commands;
import creator.loki.feature.entertainment.commands.RatingConfig;
import creator.loki.feature.entertainment.commands.RatingLevel;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import creator.loki.core.security.CooldownManager;
import net.dv8tion.jda.api.entities.Member;
import java.awt.Color;
import java.util.List;
import java.util.Random;
/**
* Fat метр - проверка на жирность
*/
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.core.security.CooldownManager;
import creator.loki.embed.EmbedFactory;
import net.dv8tion.jda.api.entities.Member;
/** Fat метр - проверка на жирность */
public class FatCommand implements Command {
private final CooldownManager cooldownManager;
private final CooldownManager cooldownManager;
public FatCommand(CooldownManager cooldownManager) {
this.cooldownManager = cooldownManager;
public FatCommand(CooldownManager cooldownManager) {
this.cooldownManager = cooldownManager;
}
@Override
public String getName() {
return "fat";
}
@Override
public String getDescription() {
return "Проверка на жирность (результат всегда одинаковый)";
}
@Override
public String getUsage() {
return "fat [@user]";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
// Проверка cooldown (5 секунд)
if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "fat")) {
long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "fat");
context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue();
return;
}
@Override
public String getName() {
return "fat";
List<Member> mentioned = context.getMessage().getMentions().getMembers();
Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0);
if (target == null) return;
// Применяем cooldown
cooldownManager.applyCooldown(context.getAuthor().getId(), "fat", 5);
// Детерминированный процент от 0 до 100
int fatLevel = getDeterministicValue(target.getId(), 999, 0, 100);
// Прогресс-бар
StringBuilder bar = new StringBuilder();
int filled = fatLevel / 10;
for (int i = 0; i < filled; i++) {
bar.append("🟩");
}
for (int i = filled; i < 10; i++) {
bar.append("⬜");
}
@Override
public String getDescription() {
return "Проверка на жирность (результат всегда одинаковый)";
}
// Определяем уровень
RatingLevel level = RatingLevel.findLevel(fatLevel, RatingConfig.getFatLevels());
@Override
public String getUsage() {
return "fat [@user]";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
// Проверка cooldown (5 секунд)
if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "fat")) {
long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "fat");
context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue();
return;
}
List<Member> mentioned = context.getMessage().getMentions().getMembers();
Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0);
if (target == null) return;
// Применяем cooldown
cooldownManager.applyCooldown(context.getAuthor().getId(), "fat", 5);
// Детерминированный процент от 0 до 100
int fatLevel = getDeterministicValue(target.getId(), 999, 0, 100);
// Прогресс-бар
StringBuilder bar = new StringBuilder();
int filled = fatLevel / 10;
for (int i = 0; i < filled; i++) {
bar.append("🟩");
}
for (int i = filled; i < 10; i++) {
bar.append("⬜");
}
// Определяем уровень
RatingLevel level = RatingLevel.findLevel(fatLevel, RatingConfig.getFatLevels());
context.getChannel().sendMessageEmbeds(
context
.getChannel()
.sendMessageEmbeds(
EmbedFactory.create()
.withTitle("🐷 Fat Метр")
.withDescription(target.getAsMention())
.addField("Жирность", bar.toString() + "\n**" + fatLevel + "%** - " + level.getComment(), false)
.addField(
"Жирность",
bar.toString() + "\n**" + fatLevel + "%** - " + level.getComment(),
false)
.withThumbnail(target.getUser().getAvatarUrl())
.withColor(level.getColor())
.withFooter("💡 Детерминировано вашим Discord ID")
.build()
).queue();
}
.build())
.queue();
}
private int getDeterministicValue(String userId, int salt, int min, int max) {
long seed = Long.parseLong(userId) + salt;
Random random = new Random(seed);
return random.nextInt(max - min + 1) + min;
}
private static final Random RANDOM = new Random();
private int getDeterministicValue(String userId, int salt, int min, int max) {
long seed = Long.parseLong(userId) + salt;
RANDOM.setSeed(seed);
return RANDOM.nextInt(max - min + 1) + min;
}
}

View file

@ -1,83 +1,83 @@
package creator.loki.feature.entertainment.commands;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import creator.loki.core.security.CooldownManager;
import net.dv8tion.jda.api.entities.Member;
import java.awt.Color;
import java.util.List;
import java.util.Random;
/**
* Gay-метр с детерминированным результатом
*/
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.core.security.CooldownManager;
import creator.loki.embed.EmbedFactory;
import net.dv8tion.jda.api.entities.Member;
/** Gay-метр с детерминированным результатом */
public class GayCommand implements Command {
private final CooldownManager cooldownManager;
private final CooldownManager cooldownManager;
public GayCommand(CooldownManager cooldownManager) {
this.cooldownManager = cooldownManager;
public GayCommand(CooldownManager cooldownManager) {
this.cooldownManager = cooldownManager;
}
@Override
public String getName() {
return "gay";
}
@Override
public String getDescription() {
return "Гей-метр (результат всегда одинаковый)";
}
@Override
public String getUsage() {
return "gay [@user]";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
// Проверка cooldown (5 секунд)
if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "gay")) {
long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "gay");
context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue();
return;
}
@Override
public String getName() {
return "gay";
List<Member> mentioned = context.getMessage().getMentions().getMembers();
Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0);
if (target == null) return;
if (target.getUser().isBot()) {
context.getChannel().sendMessage("❌ Боты не имеют ориентации!").queue();
return;
}
@Override
public String getDescription() {
return "Гей-метр (результат всегда одинаковый)";
// Применяем cooldown
cooldownManager.applyCooldown(context.getAuthor().getId(), "gay", 5);
// Детерминированный процент
int gayLevel = getDeterministicValue(target.getId(), 456, 0, 100);
// Прогресс-бар с радугой
String[] rainbow = {"🟥", "🟧", "🟨", "🟩", "🟦", "🟪"};
StringBuilder bar = new StringBuilder();
int filled = Math.min(gayLevel / 17 + 1, 6);
for (int i = 0; i < filled; i++) {
bar.append(rainbow[i]);
}
for (int i = filled; i < 6; i++) {
bar.append("⬜");
}
@Override
public String getUsage() {
return "gay [@user]";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
// Проверка cooldown (5 секунд)
if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "gay")) {
long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "gay");
context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue();
return;
}
List<Member> mentioned = context.getMessage().getMentions().getMembers();
Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0);
if (target == null) return;
if (target.getUser().isBot()) {
context.getChannel().sendMessage("❌ Боты не имеют ориентации!").queue();
return;
}
// Применяем cooldown
cooldownManager.applyCooldown(context.getAuthor().getId(), "gay", 5);
// Детерминированный процент
int gayLevel = getDeterministicValue(target.getId(), 456, 0, 100);
// Прогресс-бар с радугой
String[] rainbow = {"🟥", "🟧", "🟨", "🟩", "🟦", "🟪"};
StringBuilder bar = new StringBuilder();
int filled = Math.min(gayLevel / 17 + 1, 6);
for (int i = 0; i < filled; i++) {
bar.append(rainbow[i]);
}
for (int i = filled; i < 6; i++) {
bar.append("⬜");
}
context.getChannel().sendMessageEmbeds(
context
.getChannel()
.sendMessageEmbeds(
EmbedFactory.create()
.withTitle("🏳️‍🌈 Гей-метр")
.withDescription(target.getAsMention())
@ -85,13 +85,15 @@ public class GayCommand implements Command {
.withThumbnail(target.getUser().getAvatarUrl())
.withColor(new Color(255, 0, 255))
.withFooter("💡 Это просто мем, не воспринимайте серьезно")
.build()
).queue();
}
.build())
.queue();
}
private int getDeterministicValue(String userId, int salt, int min, int max) {
long seed = Long.parseLong(userId) + salt;
Random random = new Random(seed);
return random.nextInt(max - min + 1) + min;
}
private static final Random RANDOM = new Random();
private int getDeterministicValue(String userId, int salt, int min, int max) {
long seed = Long.parseLong(userId) + salt;
RANDOM.setSeed(seed);
return RANDOM.nextInt(max - min + 1) + min;
}
}

View file

@ -1,50 +1,54 @@
package creator.loki.feature.entertainment.commands;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import net.dv8tion.jda.api.entities.Member;
import java.awt.Color;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.stream.Collectors;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import net.dv8tion.jda.api.entities.Member;
public class GlazbobaCommand implements Command {
@Override
public String getName() {
return "glazboba";
@Override
public String getName() {
return "glazboba";
}
@Override
public String getDescription() {
return "Расширенная информация о пользователе";
}
@Override
public String getUsage() {
return "glazboba <@user>";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
List<Member> mentioned = context.getMessage().getMentions().getMembers();
if (mentioned.isEmpty()) {
context.getChannel().sendMessage("Укажи пользователя через @").queue();
return;
}
@Override
public String getDescription() {
return "Расширенная информация о пользователе";
}
Member target = mentioned.get(0);
@Override
public String getUsage() {
return "glazboba <@user>";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
List<Member> mentioned = context.getMessage().getMentions().getMembers();
if (mentioned.isEmpty()) {
context.getChannel().sendMessage("Укажи пользователя через @").queue();
return;
}
Member target = mentioned.get(0);
// Анимация загрузки
context.getChannel().sendMessage("🔍 Сканирую данные...").queue(loadingMsg -> {
try {
// Анимация загрузки
context
.getChannel()
.sendMessage("🔍 Сканирую данные...")
.queue(
loadingMsg -> {
try {
Thread.sleep(1000);
loadingMsg.editMessage("📊 Анализирую профиль...").queue();
Thread.sleep(1000);
@ -57,40 +61,43 @@ public class GlazbobaCommand implements Command {
String joinedAt = target.getTimeJoined().format(formatter);
String status = target.getOnlineStatus().name();
String nickname = target.getNickname() != null ? target.getNickname() : "Нет";
String roles = target.getRoles().stream()
.map(role -> role.getName())
.limit(10)
.collect(Collectors.joining(", "));
String roles =
target.getRoles().stream()
.map(role -> role.getName())
.limit(10)
.collect(Collectors.joining(", "));
if (roles.isEmpty()) roles = "Нет ролей";
String activities = target.getActivities().isEmpty()
? "Нет активности"
: target.getActivities().get(0).getName();
String activities =
target.getActivities().isEmpty()
? "Нет активности"
: target.getActivities().get(0).getName();
boolean isBot = target.getUser().isBot();
boolean isBoosting = target.isBoosting();
loadingMsg.editMessageEmbeds(
EmbedFactory.create()
.withTitle("👤 Информация о " + target.getEffectiveName())
.withThumbnail(target.getUser().getAvatarUrl())
.addField("🆔 ID", target.getId(), true)
.addField("📛 Никнейм", nickname, true)
.addField("🤖 Бот", isBot ? "Да" : "Нет", true)
.addField("📅 Аккаунт создан", createdAt, false)
.addField("📥 Присоединился", joinedAt, false)
.addField("🟢 Статус", status, true)
.addField("🎮 Активность", activities, true)
.addField("💎 Бустер", isBoosting ? "Да" : "Нет", true)
.addField("🎭 Роли", roles, false)
.withColor(target.getColor() != null ? target.getColor() : Color.BLUE)
.withFooter("Запросил: " + context.getAuthor().getUser().getName())
.build()
).queue();
loadingMsg
.editMessageEmbeds(
EmbedFactory.create()
.withTitle("👤 Информация о " + target.getEffectiveName())
.withThumbnail(target.getUser().getAvatarUrl())
.addField("🆔 ID", target.getId(), true)
.addField("📛 Никнейм", nickname, true)
.addField("🤖 Бот", isBot ? "Да" : "Нет", true)
.addField("📅 Аккаунт создан", createdAt, false)
.addField("📥 Присоединился", joinedAt, false)
.addField("🟢 Статус", status, true)
.addField("🎮 Активность", activities, true)
.addField("💎 Бустер", isBoosting ? "Да" : "Нет", true)
.addField("🎭 Роли", roles, false)
.withColor(target.getColor() != null ? target.getColor() : Color.BLUE)
.withFooter("Запросил: " + context.getAuthor().getUser().getName())
.build())
.queue();
} catch (InterruptedException e) {
} catch (InterruptedException e) {
loadingMsg.editMessage("❌ Ошибка загрузки").queue();
}
});
}
}
});
}
}

View file

@ -1,79 +1,76 @@
package creator.loki.feature.entertainment.commands;
import creator.loki.feature.entertainment.commands.RatingConfig;
import creator.loki.feature.entertainment.commands.RatingLevel;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import creator.loki.core.security.CooldownManager;
import net.dv8tion.jda.api.entities.Member;
import java.awt.Color;
import java.util.List;
import java.util.Random;
/**
* IQ тест с детерминированным результатом
*/
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.core.security.CooldownManager;
import creator.loki.embed.EmbedFactory;
import net.dv8tion.jda.api.entities.Member;
/** IQ тест с детерминированным результатом */
public class IqCommand implements Command {
private final CooldownManager cooldownManager;
private final CooldownManager cooldownManager;
public IqCommand(CooldownManager cooldownManager) {
this.cooldownManager = cooldownManager;
public IqCommand(CooldownManager cooldownManager) {
this.cooldownManager = cooldownManager;
}
@Override
public String getName() {
return "iq";
}
@Override
public String getDescription() {
return "Тест IQ (результат всегда одинаковый)";
}
@Override
public String getUsage() {
return "iq [@user]";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
// Проверка cooldown (5 секунд)
if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "iq")) {
long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "iq");
context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue();
return;
}
@Override
public String getName() {
return "iq";
List<Member> mentioned = context.getMessage().getMentions().getMembers();
Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0);
if (target == null) return;
// Применяем cooldown
cooldownManager.applyCooldown(context.getAuthor().getId(), "iq", 5);
int iq;
if (target.getUser().isBot()) {
iq = 9000; // Боты умные!
} else if (target.getId().equals("1414975197269987471")) {
iq = 1000; // Специальное значение
} else {
// Детерминированный IQ от 0 до 140
iq = getDeterministicValue(target.getId(), 123, 0, 140);
}
@Override
public String getDescription() {
return "Тест IQ (результат всегда одинаковый)";
}
// Определяем уровень
RatingLevel level = RatingLevel.findLevel(iq, RatingConfig.getIqLevels());
@Override
public String getUsage() {
return "iq [@user]";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
// Проверка cooldown (5 секунд)
if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "iq")) {
long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "iq");
context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue();
return;
}
List<Member> mentioned = context.getMessage().getMentions().getMembers();
Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0);
if (target == null) return;
// Применяем cooldown
cooldownManager.applyCooldown(context.getAuthor().getId(), "iq", 5);
int iq;
if (target.getUser().isBot()) {
iq = 9000; // Боты умные!
} else if (target.getId().equals("1414975197269987471")) {
iq = 1000; // Специальное значение
} else {
// Детерминированный IQ от 0 до 140
iq = getDeterministicValue(target.getId(), 123, 0, 140);
}
// Определяем уровень
RatingLevel level = RatingLevel.findLevel(iq, RatingConfig.getIqLevels());
context.getChannel().sendMessageEmbeds(
context
.getChannel()
.sendMessageEmbeds(
EmbedFactory.create()
.withTitle("🧠 Тест IQ")
.withDescription("Результат для " + target.getAsMention())
@ -81,13 +78,15 @@ public class IqCommand implements Command {
.withThumbnail(target.getUser().getAvatarUrl())
.withColor(level.getColor())
.withFooter("💡 Научно доказано вашим ID")
.build()
).queue();
}
.build())
.queue();
}
private int getDeterministicValue(String userId, int salt, int min, int max) {
long seed = Long.parseLong(userId) + salt;
Random random = new Random(seed);
return random.nextInt(max - min + 1) + min;
}
private static final Random RANDOM = new Random();
private int getDeterministicValue(String userId, int salt, int min, int max) {
long seed = Long.parseLong(userId) + salt;
RANDOM.setSeed(seed);
return RANDOM.nextInt(max - min + 1) + min;
}
}

View file

@ -1,53 +1,60 @@
package creator.loki.feature.entertainment.commands;
import java.awt.Color;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import java.awt.Color;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URI;
public class MemeCommand implements Command {
@Override
public String getName() {
return "meme";
}
@Override
public String getName() {
return "meme";
}
@Override
public String getDescription() {
return "Случайный мем";
}
@Override
public String getDescription() {
return "Случайный мем";
}
@Override
public String getUsage() {
return "meme";
}
@Override
public String getUsage() {
return "meme";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
context.getChannel().sendMessage("🔍 Ищу мем...").queue(loadingMsg -> {
try {
@Override
public void execute(CommandContext context) {
context
.getChannel()
.sendMessage("🔍 Ищу мем...")
.queue(
loadingMsg -> {
try {
var url = new URI("https://meme-api.com/gimme").toURL();
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("User-Agent", "Mozilla/5.0");
BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
BufferedReader in =
new BufferedReader(
new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8));
StringBuilder response = new StringBuilder();
String line;
while ((line = in.readLine()) != null) {
response.append(line);
response.append(line);
}
in.close();
@ -56,18 +63,19 @@ public class MemeCommand implements Command {
String title = json.get("title").getAsString();
String subreddit = json.get("subreddit").getAsString();
loadingMsg.editMessageEmbeds(
EmbedFactory.create()
.withTitle(title)
.withImage(memeUrl)
.withColor(new Color(255, 69, 0))
.withFooter("r/" + subreddit)
.build()
).queue();
loadingMsg
.editMessageEmbeds(
EmbedFactory.create()
.withTitle(title)
.withImage(memeUrl)
.withColor(new Color(255, 69, 0))
.withFooter("r/" + subreddit)
.build())
.queue();
} catch (Exception e) {
} catch (Exception e) {
loadingMsg.editMessage("❌ Не удалось загрузить мем: " + e.getMessage()).queue();
}
});
}
}
});
}
}

View file

@ -1,105 +1,107 @@
package creator.loki.feature.entertainment.commands;
import creator.loki.feature.entertainment.commands.RatingConfig;
import creator.loki.feature.entertainment.commands.RatingLevel;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import creator.loki.core.security.CooldownManager;
import net.dv8tion.jda.api.entities.Member;
import java.awt.Color;
import java.util.List;
import java.util.Random;
/**
* Mustache метр - проверка на усатость
*/
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.core.security.CooldownManager;
import creator.loki.embed.EmbedFactory;
import net.dv8tion.jda.api.entities.Member;
/** Mustache метр - проверка на усатость */
public class MustacheCommand implements Command {
private final CooldownManager cooldownManager;
private final CooldownManager cooldownManager;
public MustacheCommand(CooldownManager cooldownManager) {
this.cooldownManager = cooldownManager;
public MustacheCommand(CooldownManager cooldownManager) {
this.cooldownManager = cooldownManager;
}
@Override
public String getName() {
return "mustache";
}
@Override
public String getDescription() {
return "Проверка на усатость (результат всегда одинаковый)";
}
@Override
public String getUsage() {
return "mustache [@user]";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
// Проверка cooldown (5 секунд)
if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "mustache")) {
long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "mustache");
context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue();
return;
}
@Override
public String getName() {
return "mustache";
List<Member> mentioned = context.getMessage().getMentions().getMembers();
Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0);
if (target == null) return;
// Применяем cooldown
cooldownManager.applyCooldown(context.getAuthor().getId(), "mustache", 5);
int mustacheLevel;
// Специальные значения
if (target.getUser().isBot() || target.getId().equals("1414975197269987471")) {
mustacheLevel = 0; // Боты и специальный ID = 0%
} else if (target.getId().equals("678283217094180915")) {
mustacheLevel = 1000; // Специальный ID = 1000%
} else {
// Детерминированный процент от 0 до 100
mustacheLevel = getDeterministicValue(target.getId(), 888, 0, 100);
}
@Override
public String getDescription() {
return "Проверка на усатость (результат всегда одинаковый)";
// Прогресс-бар с усами
StringBuilder bar = new StringBuilder();
int filled = Math.min(mustacheLevel / 10, 10);
for (int i = 0; i < filled; i++) {
bar.append("🧔");
}
for (int i = filled; i < 10; i++) {
bar.append("⬜");
}
@Override
public String getUsage() {
return "mustache [@user]";
}
// Определяем уровень
RatingLevel level = RatingLevel.findLevel(mustacheLevel, RatingConfig.getMustacheLevels());
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
// Проверка cooldown (5 секунд)
if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "mustache")) {
long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "mustache");
context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue();
return;
}
List<Member> mentioned = context.getMessage().getMentions().getMembers();
Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0);
if (target == null) return;
// Применяем cooldown
cooldownManager.applyCooldown(context.getAuthor().getId(), "mustache", 5);
int mustacheLevel;
// Специальные значения
if (target.getUser().isBot() || target.getId().equals("1414975197269987471")) {
mustacheLevel = 0; // Боты и специальный ID = 0%
} else if (target.getId().equals("678283217094180915")) {
mustacheLevel = 1000; // Специальный ID = 1000%
} else {
// Детерминированный процент от 0 до 100
mustacheLevel = getDeterministicValue(target.getId(), 888, 0, 100);
}
// Прогресс-бар с усами
StringBuilder bar = new StringBuilder();
int filled = Math.min(mustacheLevel / 10, 10);
for (int i = 0; i < filled; i++) {
bar.append("🧔");
}
for (int i = filled; i < 10; i++) {
bar.append("⬜");
}
// Определяем уровень
RatingLevel level = RatingLevel.findLevel(mustacheLevel, RatingConfig.getMustacheLevels());
context.getChannel().sendMessageEmbeds(
context
.getChannel()
.sendMessageEmbeds(
EmbedFactory.create()
.withTitle("🧔 Mustache Метр")
.withDescription(target.getAsMention())
.addField("Усатость", bar.toString() + "\n**" + mustacheLevel + "%** - " + level.getComment(), false)
.addField(
"Усатость",
bar.toString() + "\n**" + mustacheLevel + "%** - " + level.getComment(),
false)
.withThumbnail(target.getUser().getAvatarUrl())
.withColor(level.getColor())
.withFooter("💡 Детерминировано вашим Discord ID")
.build()
).queue();
}
.build())
.queue();
}
private int getDeterministicValue(String userId, int salt, int min, int max) {
long seed = Long.parseLong(userId) + salt;
Random random = new Random(seed);
return random.nextInt(max - min + 1) + min;
}
private static final Random RANDOM = new Random();
private int getDeterministicValue(String userId, int salt, int min, int max) {
long seed = Long.parseLong(userId) + salt;
RANDOM.setSeed(seed);
return RANDOM.nextInt(max - min + 1) + min;
}
}

View file

@ -1,104 +1,111 @@
package creator.loki.feature.entertainment.commands;
import creator.loki.feature.entertainment.commands.RatingConfig;
import creator.loki.feature.entertainment.commands.RatingLevel;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import creator.loki.core.security.CooldownManager;
import net.dv8tion.jda.api.entities.Member;
import java.awt.Color;
import java.util.List;
import java.util.Random;
/**
* PP размер с детерминированным результатом
*/
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.core.security.CooldownManager;
import creator.loki.embed.EmbedFactory;
import net.dv8tion.jda.api.entities.Member;
/** PP размер с детерминированным результатом */
public class PpCommand implements Command {
private final CooldownManager cooldownManager;
private final CooldownManager cooldownManager;
public PpCommand(CooldownManager cooldownManager) {
this.cooldownManager = cooldownManager;
public PpCommand(CooldownManager cooldownManager) {
this.cooldownManager = cooldownManager;
}
@Override
public String getName() {
return "pp";
}
@Override
public String getDescription() {
return "Измеритель pp (результат всегда одинаковый)";
}
@Override
public String getUsage() {
return "pp [@user]";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
// Проверка cooldown (5 секунд)
if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "pp")) {
long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "pp");
context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue();
return;
}
@Override
public String getName() {
return "pp";
List<Member> mentioned = context.getMessage().getMentions().getMembers();
Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0);
if (target == null) return;
if (target.getUser().isBot()) {
context.getChannel().sendMessage("❌ У ботов нет pp!").queue();
return;
}
@Override
public String getDescription() {
return "Измеритель pp (результат всегда одинаковый)";
// Применяем cooldown
cooldownManager.applyCooldown(context.getAuthor().getId(), "pp", 5);
// Специальное значение для конкретного пользователя
int size;
if (target.getId().equals("1414975197269987471")) {
size = 100;
} else {
// Детерминированный размер от 0 до 30 см
size = getDeterministicValue(target.getId(), 6969, 0, 30);
}
@Override
public String getUsage() {
return "pp [@user]";
// Визуализация
StringBuilder ppVisual = new StringBuilder("8");
for (int i = 0; i < size; i++) {
ppVisual.append("=");
}
ppVisual.append("D");
@Override
public String getCategory() {
return "Развлечения";
}
// Определяем уровень
RatingLevel level = RatingLevel.findLevel(size, RatingConfig.getPpLevels());
@Override
public void execute(CommandContext context) {
// Проверка cooldown (5 секунд)
if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "pp")) {
long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "pp");
context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue();
return;
}
List<Member> mentioned = context.getMessage().getMentions().getMembers();
Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0);
if (target == null) return;
if (target.getUser().isBot()) {
context.getChannel().sendMessage("❌ У ботов нет pp!").queue();
return;
}
// Применяем cooldown
cooldownManager.applyCooldown(context.getAuthor().getId(), "pp", 5);
// Специальное значение для конкретного пользователя
int size;
if (target.getId().equals("1414975197269987471")) {
size = 100;
} else {
// Детерминированный размер от 0 до 30 см
size = getDeterministicValue(target.getId(), 6969, 0, 30);
}
// Визуализация
StringBuilder ppVisual = new StringBuilder("8");
for (int i = 0; i < size; i++) {
ppVisual.append("=");
}
ppVisual.append("D");
// Определяем уровень
RatingLevel level = RatingLevel.findLevel(size, RatingConfig.getPpLevels());
context.getChannel().sendMessageEmbeds(
context
.getChannel()
.sendMessageEmbeds(
EmbedFactory.create()
.withTitle("🍆 PP Размер")
.withDescription(target.getAsMention())
.addField("Размер", "```" + ppVisual.toString() + "```\n**" + size + " см** - " + level.getComment(), false)
.addField(
"Размер",
"```"
+ ppVisual.toString()
+ "```\n**"
+ size
+ " см** - "
+ level.getComment(),
false)
.withThumbnail(target.getUser().getAvatarUrl())
.withColor(level.getColor())
.withFooter("💡 Детерминировано вашим Discord ID")
.build()
).queue();
}
.build())
.queue();
}
private int getDeterministicValue(String userId, int salt, int min, int max) {
long seed = Long.parseLong(userId) + salt;
Random random = new Random(seed);
return random.nextInt(max - min + 1) + min;
}
private static final Random RANDOM = new Random();
private int getDeterministicValue(String userId, int salt, int min, int max) {
long seed = Long.parseLong(userId) + salt;
RANDOM.setSeed(seed);
return RANDOM.nextInt(max - min + 1) + min;
}
}

View file

@ -1,135 +1,146 @@
package creator.loki.feature.entertainment.commands;
import java.awt.Color;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.config.BotConfig;
import creator.loki.embed.EmbedTemplates;
import net.dv8tion.jda.api.EmbedBuilder;
import java.awt.Color;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
/**
* Случайный выбор из вариантов. Разбор строки - {@link ChoiceOptionParser}.
*/
/** Случайный выбор из вариантов. Разбор строки - {@link ChoiceOptionParser}. */
public class RandomChoiceCommand implements Command {
public enum Flavor {
DEFAULT(
"Выбери один вариант - рандом решит за тебя.",
new String[] {
"🎲 Выпало: **%s**",
"🎯 Окей, берём **%s**",
"✨ Вселенная шепчет: **%s**",
"🍀 Фортуна на твоей стороне - **%s**",
"📌 Без лишних слов: **%s**"
}
),
WHEEL(
"Колесо фортуны (тот же рандом, но драматичнее).",
new String[] {
"🎡 Колесо остановилось на **%s**",
"🎪 Барабанная дробь… **%s**!",
"🎠 Карусель выбрала **%s**",
"⚡ Судьба указывает на **%s**"
}
),
ORACLE(
"Оракул в шутливом тоне.",
new String[] {
"🔮 Оракул видит… **%s**",
"🌙 Звёзды сложились: **%s**",
"✴️ Судьба-печенька говорит: **%s**",
"🧿 Сглаз обойдёт - бери **%s** и не оглядывайся"
}
);
public enum Flavor {
DEFAULT(
"Выбери один вариант - рандом решит за тебя.",
new String[] {
"🎲 Выпало: **%s**",
"🎯 Окей, берём **%s**",
"✨ Вселенная шепчет: **%s**",
"🍀 Фортуна на твоей стороне - **%s**",
"📌 Без лишних слов: **%s**"
}),
WHEEL(
"Колесо фортуны (тот же рандом, но драматичнее).",
new String[] {
"🎡 Колесо остановилось на **%s**",
"🎪 Барабанная дробь… **%s**!",
"🎠 Карусель выбрала **%s**",
"⚡ Судьба указывает на **%s**"
}),
ORACLE(
"Оракул в шутливом тоне.",
new String[] {
"🔮 Оракул видит… **%s**",
"🌙 Звёзды сложились: **%s**",
"✴️ Судьба-печенька говорит: **%s**",
"🧿 Сглаз обойдёт - бери **%s** и не оглядывайся"
});
private final String blurb;
private final String[] templates;
private final String blurb;
private final String[] templates;
Flavor(String blurb, String[] templates) {
this.blurb = blurb;
this.templates = templates;
}
String randomLine(String pick) {
var rnd = ThreadLocalRandom.current();
return String.format(templates[rnd.nextInt(templates.length)], pick);
}
Flavor(String blurb, String[] templates) {
this.blurb = blurb;
this.templates = templates;
}
private final BotConfig config;
private final String commandName;
private final Flavor flavor;
String randomLine(String pick) {
var rnd = ThreadLocalRandom.current();
return String.format(templates[rnd.nextInt(templates.length)], pick);
}
}
public RandomChoiceCommand(BotConfig config, String commandName, Flavor flavor) {
this.config = config;
this.commandName = commandName;
this.flavor = flavor;
private final BotConfig config;
private final String commandName;
private final Flavor flavor;
public RandomChoiceCommand(BotConfig config, String commandName, Flavor flavor) {
this.config = config;
this.commandName = commandName;
this.flavor = flavor;
}
@Override
public String getName() {
return commandName;
}
@Override
public String getDescription() {
return flavor.blurb;
}
@Override
public String getUsage() {
return commandName + " вариант1 | вариант2 | …";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
String p = config.getPrefix();
String tail =
ChoiceOptionParser.tailAfterCommand(context.getMessage().getContentRaw(), p, commandName);
if (tail.isBlank()) {
reply(context, EmbedTemplates.error("Нужны варианты", usageExamples(p)));
return;
}
@Override
public String getName() {
return commandName;
List<String> options = ChoiceOptionParser.parseOptions(tail);
if (options.size() < 2) {
reply(
context,
EmbedTemplates.error(
"Мало вариантов",
"Минимум **2** разных варианта: `|`, запятая, `или`, `/` или слова через пробел."));
return;
}
@Override
public String getDescription() {
return flavor.blurb;
}
var rnd = ThreadLocalRandom.current();
String pick = options.get(rnd.nextInt(options.size()));
String line = flavor.randomLine(pick);
String listed =
options.stream().map(s -> "`" + s + "`").reduce((a, b) -> a + " · " + b).orElse("");
@Override
public String getUsage() {
return commandName + " вариант1 | вариант2 | …";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
String p = config.getPrefix();
String tail = ChoiceOptionParser.tailAfterCommand(context.getMessage().getContentRaw(), p, commandName);
if (tail.isBlank()) {
reply(context, EmbedTemplates.error("Нужны варианты", usageExamples(p)));
return;
}
List<String> options = ChoiceOptionParser.parseOptions(tail);
if (options.size() < 2) {
reply(context, EmbedTemplates.error(
"Мало вариантов",
"Минимум **2** разных варианта: `|`, запятая, `или`, `/` или слова через пробел."
));
return;
}
var rnd = ThreadLocalRandom.current();
String pick = options.get(rnd.nextInt(options.size()));
String line = flavor.randomLine(pick);
String listed = options.stream().map(s -> "`" + s + "`").reduce((a, b) -> a + " · " + b).orElse("");
var embed = new EmbedBuilder()
var embed =
new EmbedBuilder()
.setTitle("🎲 " + commandName)
.setDescription(line + "\n\n*Варианты:* " + listed)
.setColor(new Color(88, 101, 242))
.setFooter("choose · pick · decide · wheel");
reply(context, embed.build());
}
reply(context, embed.build());
}
private static void reply(CommandContext ctx, net.dv8tion.jda.api.entities.MessageEmbed embed) {
ctx.getChannel().sendMessageEmbeds(embed).queue();
}
private static void reply(CommandContext ctx, net.dv8tion.jda.api.entities.MessageEmbed embed) {
ctx.getChannel().sendMessageEmbeds(embed).queue();
}
private String usageExamples(String p) {
return "**Примеры:**\n"
+ "`" + p + commandName + " пицца | суши | бургер`\n"
+ "`" + p + commandName + " чай, кофе, сок`\n"
+ "`" + p + commandName + " дома или в клуб`\n"
+ "`" + p + commandName + " да / нет / может`";
}
private String usageExamples(String p) {
return "**Примеры:**\n"
+ "`"
+ p
+ commandName
+ " пицца | суши | бургер`\n"
+ "`"
+ p
+ commandName
+ " чай, кофе, сок`\n"
+ "`"
+ p
+ commandName
+ " дома или в клуб`\n"
+ "`"
+ p
+ commandName
+ " да / нет / может`";
}
}

View file

@ -1,101 +1,103 @@
package creator.loki.feature.entertainment.commands;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import creator.loki.core.security.CooldownManager;
import net.dv8tion.jda.api.entities.Member;
import java.awt.Color;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
/**
* Оценка пользователя из 10 с детерминированным результатом
*/
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.core.security.CooldownManager;
import creator.loki.embed.EmbedFactory;
import net.dv8tion.jda.api.entities.Member;
/** Оценка пользователя из 10 с детерминированным результатом */
public class RateCommand implements Command {
private final CooldownManager cooldownManager;
private final CooldownManager cooldownManager;
public RateCommand(CooldownManager cooldownManager) {
this.cooldownManager = cooldownManager;
public RateCommand(CooldownManager cooldownManager) {
this.cooldownManager = cooldownManager;
}
@Override
public String getName() {
return "rate";
}
@Override
public String getDescription() {
return "Оценить пользователя из 10 (результат всегда одинаковый)";
}
@Override
public String getUsage() {
return "rate [@user]";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
// Проверка cooldown (5 секунд)
if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "rate")) {
long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "rate");
context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue();
return;
}
@Override
public String getName() {
return "rate";
}
List<Member> mentioned = context.getMessage().getMentions().getMembers();
Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0);
@Override
public String getDescription() {
return "Оценить пользователя из 10 (результат всегда одинаковый)";
}
if (target == null) return;
@Override
public String getUsage() {
return "rate [@user]";
}
// Применяем cooldown
cooldownManager.applyCooldown(context.getAuthor().getId(), "rate", 5);
@Override
public String getCategory() {
return "Развлечения";
}
// Детерминированная оценка от 1 до 10
int rating = getDeterministicValue(target.getId(), 420, 1, 10);
@Override
public void execute(CommandContext context) {
// Проверка cooldown (5 секунд)
if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "rate")) {
long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "rate");
context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue();
return;
}
// Визуализация звездами
String stars = "⭐".repeat(rating) + "☆".repeat(10 - rating);
List<Member> mentioned = context.getMessage().getMentions().getMembers();
Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0);
// Комментарий
Map<Integer, String> comments = new HashMap<>();
comments.put(10, "💯 ИДЕАЛЬНО!");
comments.put(9, "🔥 Почти идеально!");
comments.put(8, "😍 Отлично!");
comments.put(7, "👍 Хорошо!");
comments.put(6, "🙂 Неплохо");
comments.put(5, "😐 Средне");
comments.put(4, "😕 Ниже среднего");
comments.put(3, "😬 Так себе");
comments.put(2, "💀 Плохо");
comments.put(1, "🗿 Ужасно");
if (target == null) return;
String comment = comments.getOrDefault(rating, "🤔");
Color color = rating >= 8 ? new Color(255, 215, 0) : new Color(52, 152, 219);
// Применяем cooldown
cooldownManager.applyCooldown(context.getAuthor().getId(), "rate", 5);
// Детерминированная оценка от 1 до 10
int rating = getDeterministicValue(target.getId(), 420, 1, 10);
// Визуализация звездами
String stars = "⭐".repeat(rating) + "☆".repeat(10 - rating);
// Комментарий
Map<Integer, String> comments = new HashMap<>();
comments.put(10, "💯 ИДЕАЛЬНО!");
comments.put(9, "🔥 Почти идеально!");
comments.put(8, "😍 Отлично!");
comments.put(7, "👍 Хорошо!");
comments.put(6, "🙂 Неплохо");
comments.put(5, "😐 Средне");
comments.put(4, "😕 Ниже среднего");
comments.put(3, "😬 Так себе");
comments.put(2, "💀 Плохо");
comments.put(1, "🗿 Ужасно");
String comment = comments.getOrDefault(rating, "🤔");
Color color = rating >= 8 ? new Color(255, 215, 0) : new Color(52, 152, 219);
context.getChannel().sendMessageEmbeds(
context
.getChannel()
.sendMessageEmbeds(
EmbedFactory.create()
.withTitle("⭐ Оценка")
.withDescription(target.getAsMention())
.addField("Рейтинг", stars + "\n**" + rating + "/10** - " + comment, false)
.withThumbnail(target.getUser().getAvatarUrl())
.withColor(color)
.build()
).queue();
}
.build())
.queue();
}
private int getDeterministicValue(String userId, int salt, int min, int max) {
long seed = Long.parseLong(userId) + salt;
Random random = new Random(seed);
return random.nextInt(max - min + 1) + min;
}
private static final Random RANDOM = new Random();
private int getDeterministicValue(String userId, int salt, int min, int max) {
long seed = Long.parseLong(userId) + salt;
RANDOM.setSeed(seed);
return RANDOM.nextInt(max - min + 1) + min;
}
}

View file

@ -4,78 +4,61 @@ import java.awt.Color;
import java.util.Arrays;
import java.util.List;
/**
* Конфигурации уровней для различных rating команд
*/
/** Конфигурации уровней для различных rating команд */
public class RatingConfig {
/**
* Уровни для Mustache команды
*/
public static List<RatingLevel> getMustacheLevels() {
return Arrays.asList(
new RatingLevel(1000, "🎩 ЛЕГЕНДАРНЫЕ УСИЩИ!", new Color(255, 215, 0)),
new RatingLevel(80, "🧔 Роскошные усы!", new Color(139, 69, 19)),
new RatingLevel(60, "👨 Хорошие усы", new Color(160, 82, 45)),
new RatingLevel(40, "😊 Усики растут", new Color(205, 133, 63)),
new RatingLevel(20, "🙂 Лёгкая щетина", new Color(210, 180, 140)),
new RatingLevel(1, "😐 Почти нет усов", new Color(128, 128, 128)),
new RatingLevel(0, "🪒 Гладко выбрит", new Color(200, 200, 200))
);
}
/** Уровни для Mustache команды */
public static List<RatingLevel> getMustacheLevels() {
return Arrays.asList(
new RatingLevel(1000, "🎩 ЛЕГЕНДАРНЫЕ УСИЩИ!", new Color(255, 215, 0)),
new RatingLevel(80, "🧔 Роскошные усы!", new Color(139, 69, 19)),
new RatingLevel(60, "👨 Хорошие усы", new Color(160, 82, 45)),
new RatingLevel(40, "😊 Усики растут", new Color(205, 133, 63)),
new RatingLevel(20, "🙂 Лёгкая щетина", new Color(210, 180, 140)),
new RatingLevel(1, "😐 Почти нет усов", new Color(128, 128, 128)),
new RatingLevel(0, "🪒 Гладко выбрит", new Color(200, 200, 200)));
}
/**
* Уровни для IQ команды
*/
public static List<RatingLevel> getIqLevels() {
return Arrays.asList(
new RatingLevel(140, "🧠 Гений!", new Color(255, 215, 0)),
new RatingLevel(120, "🎓 Очень умный", new Color(100, 255, 150)),
new RatingLevel(100, "📚 Средний", new Color(52, 152, 219)),
new RatingLevel(80, "📖 Ниже среднего", new Color(255, 165, 0)),
new RatingLevel(0, "🤔 Низкий", new Color(231, 76, 60))
);
}
/** Уровни для IQ команды */
public static List<RatingLevel> getIqLevels() {
return Arrays.asList(
new RatingLevel(140, "🧠 Гений!", new Color(255, 215, 0)),
new RatingLevel(120, "🎓 Очень умный", new Color(100, 255, 150)),
new RatingLevel(100, "📚 Средний", new Color(52, 152, 219)),
new RatingLevel(80, "📖 Ниже среднего", new Color(255, 165, 0)),
new RatingLevel(0, "🤔 Низкий", new Color(231, 76, 60)));
}
/**
* Уровни для Fat команды
*/
public static List<RatingLevel> getFatLevels() {
return Arrays.asList(
new RatingLevel(90, "🐷 СВИНОТА!", new Color(255, 0, 0)),
new RatingLevel(70, "🍔 Очень жирный", new Color(255, 100, 0)),
new RatingLevel(50, "🍕 Жирноват", new Color(255, 165, 0)),
new RatingLevel(30, "👍 Нормальный вес", new Color(0, 200, 0)),
new RatingLevel(10, "🥗 Худой", new Color(100, 200, 255)),
new RatingLevel(0, "💀 Очень худой", new Color(150, 150, 150))
);
}
/** Уровни для Fat команды */
public static List<RatingLevel> getFatLevels() {
return Arrays.asList(
new RatingLevel(90, "🐷 СВИНОТА!", new Color(255, 0, 0)),
new RatingLevel(70, "🍔 Очень жирный", new Color(255, 100, 0)),
new RatingLevel(50, "🍕 Жирноват", new Color(255, 165, 0)),
new RatingLevel(30, "👍 Нормальный вес", new Color(0, 200, 0)),
new RatingLevel(10, "🥗 Худой", new Color(100, 200, 255)),
new RatingLevel(0, "💀 Очень худой", new Color(150, 150, 150)));
}
/**
* Уровни для PP команды (размер в см)
*/
public static List<RatingLevel> getPpLevels() {
return Arrays.asList(
new RatingLevel(25, "😳 НЕВЕРОЯТНО!", new Color(255, 215, 0)),
new RatingLevel(20, "😮 Впечатляет", new Color(100, 255, 150)),
new RatingLevel(15, "👍 Выше среднего", new Color(52, 152, 219)),
new RatingLevel(10, "🙂 Средний", new Color(128, 128, 128)),
new RatingLevel(5, "😐 Ниже среднего", new Color(255, 165, 0)),
new RatingLevel(0, "💀 Микро", new Color(231, 76, 60))
);
}
/** Уровни для PP команды (размер в см) */
public static List<RatingLevel> getPpLevels() {
return Arrays.asList(
new RatingLevel(25, "😳 НЕВЕРОЯТНО!", new Color(255, 215, 0)),
new RatingLevel(20, "😮 Впечатляет", new Color(100, 255, 150)),
new RatingLevel(15, "👍 Выше среднего", new Color(52, 152, 219)),
new RatingLevel(10, "🙂 Средний", new Color(128, 128, 128)),
new RatingLevel(5, "😐 Ниже среднего", new Color(255, 165, 0)),
new RatingLevel(0, "💀 Микро", new Color(231, 76, 60)));
}
/**
* Уровни для Boykisser команды
*/
public static List<RatingLevel> getBoykisserLevels() {
return Arrays.asList(
new RatingLevel(90, "💋 ЛЕГЕНДАРНЫЙ BOYKISSER!", new Color(255, 105, 180)),
new RatingLevel(70, "😘 Активный boykisser", new Color(255, 182, 193)),
new RatingLevel(50, "😊 Любит целоваться", new Color(255, 192, 203)),
new RatingLevel(30, "🙂 Иногда целуется", new Color(255, 218, 185)),
new RatingLevel(10, "😐 Редко целуется", new Color(176, 196, 222)),
new RatingLevel(0, "😶 Не целуется", new Color(128, 128, 128))
);
}
/** Уровни для Boykisser команды */
public static List<RatingLevel> getBoykisserLevels() {
return Arrays.asList(
new RatingLevel(90, "💋 ЛЕГЕНДАРНЫЙ BOYKISSER!", new Color(255, 105, 180)),
new RatingLevel(70, "😘 Активный boykisser", new Color(255, 182, 193)),
new RatingLevel(50, "😊 Любит целоваться", new Color(255, 192, 203)),
new RatingLevel(30, "🙂 Иногда целуется", new Color(255, 218, 185)),
new RatingLevel(10, "😐 Редко целуется", new Color(176, 196, 222)),
new RatingLevel(0, "😶 Не целуется", new Color(128, 128, 128)));
}
}

View file

@ -3,42 +3,41 @@ package creator.loki.feature.entertainment.commands;
import java.awt.Color;
import java.util.List;
/**
* Уровень рейтинга с порогом, комментарием и цветом
*/
/** Уровень рейтинга с порогом, комментарием и цветом */
public class RatingLevel {
private final int threshold;
private final String comment;
private final Color color;
private final int threshold;
private final String comment;
private final Color color;
public RatingLevel(int threshold, String comment, Color color) {
this.threshold = threshold;
this.comment = comment;
this.color = color;
}
public RatingLevel(int threshold, String comment, Color color) {
this.threshold = threshold;
this.comment = comment;
this.color = color;
}
public int getThreshold() {
return threshold;
}
public int getThreshold() {
return threshold;
}
public String getComment() {
return comment;
}
public String getComment() {
return comment;
}
public Color getColor() {
return color;
}
public Color getColor() {
return color;
}
/**
* Находит подходящий уровень для значения
* @param value значение для проверки
* @param levels список уровней (должен быть отсортирован по убыванию threshold)
* @return подходящий уровень
*/
public static RatingLevel findLevel(int value, List<RatingLevel> levels) {
return levels.stream()
.filter(level -> value >= level.threshold)
.findFirst()
.orElse(levels.get(levels.size() - 1));
}
/**
* Находит подходящий уровень для значения
*
* @param value значение для проверки
* @param levels список уровней (должен быть отсортирован по убыванию threshold)
* @return подходящий уровень
*/
public static RatingLevel findLevel(int value, List<RatingLevel> levels) {
return levels.stream()
.filter(level -> value >= level.threshold)
.findFirst()
.orElse(levels.get(levels.size() - 1));
}
}

View file

@ -1,5 +1,10 @@
package creator.loki.feature.help;
import java.awt.Color;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import creator.loki.command.Command;
@ -12,138 +17,136 @@ import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionE
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import net.dv8tion.jda.api.interactions.components.selections.StringSelectMenu;
import java.awt.Color;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Singleton
public class HelpMenuHandler extends ListenerAdapter {
private final CommandRegistry registry;
private final BotConfig config;
private final CommandRegistry registry;
private final BotConfig config;
@Inject
public HelpMenuHandler(CommandRegistry registry, BotConfig config) {
this.registry = registry;
this.config = config;
@Inject
public HelpMenuHandler(CommandRegistry registry, BotConfig config) {
this.registry = registry;
this.config = config;
}
@Override
public void onStringSelectInteraction(StringSelectInteractionEvent event) {
if (!event.getComponentId().equals("help:category")) {
return;
}
@Override
public void onStringSelectInteraction(StringSelectInteractionEvent event) {
if (!event.getComponentId().equals("help:category")) {
return;
}
String selectedValue = event.getValues().get(0);
if (!selectedValue.startsWith("category:")) {
return;
}
String selectedValue = event.getValues().get(0);
if (!selectedValue.startsWith("category:")) {
return;
}
String category = selectedValue.substring("category:".length());
String category = selectedValue.substring("category:".length());
// Проверка прав для категории "Настройка"
if ("Настройка".equals(category)
&& event.getMember() != null
&& !event.getMember().hasPermission(Permission.ADMINISTRATOR)) {
event.reply("❌ Эта категория доступна только администраторам!").setEphemeral(true).queue();
return;
}
// Проверка прав для категории "Настройка"
if ("Настройка".equals(category) && event.getMember() != null
&& !event.getMember().hasPermission(Permission.ADMINISTRATOR)) {
event.reply("❌ Эта категория доступна только администраторам!")
.setEphemeral(true)
.queue();
return;
}
if ("Супер-Админ".equals(category)
&& !config.getSuperAdminId().equals(event.getUser().getId())) {
event.reply("❌ Эта категория доступна только владельцу бота.").setEphemeral(true).queue();
return;
}
if ("Супер-Админ".equals(category) && !config.getSuperAdminId().equals(event.getUser().getId())) {
event.reply("❌ Эта категория доступна только владельцу бота.")
.setEphemeral(true)
.queue();
return;
}
var commands = registry.getAllCommands();
Map<String, List<Command>> commandsByCategory =
commands.stream().collect(Collectors.groupingBy(Command::getCategory));
var commands = registry.getAllCommands();
Map<String, List<Command>> commandsByCategory = commands.stream()
.collect(Collectors.groupingBy(Command::getCategory));
List<Command> categoryCommands = commandsByCategory.get(category);
if (categoryCommands == null || categoryCommands.isEmpty()) {
event.reply("❌ В этой категории нет команд!").setEphemeral(true).queue();
return;
}
List<Command> categoryCommands = commandsByCategory.get(category);
if (categoryCommands == null || categoryCommands.isEmpty()) {
event.reply("❌ В этой категории нет команд!")
.setEphemeral(true)
.queue();
return;
}
// Создаём embed для категории
EmbedFactory embed = createCategoryEmbed(category, categoryCommands, event);
// Создаём embed для категории
EmbedFactory embed = createCategoryEmbed(category, categoryCommands, event);
// Пересоздаём селектор
StringSelectMenu.Builder menuBuilder =
StringSelectMenu.create("help:category").setPlaceholder("📂 Выберите категорию");
// Пересоздаём селектор
StringSelectMenu.Builder menuBuilder = StringSelectMenu.create("help:category")
.setPlaceholder("📂 Выберите категорию");
boolean isAdmin =
event.getMember() != null && event.getMember().hasPermission(Permission.ADMINISTRATOR);
boolean isSuper = config.getSuperAdminId().equals(event.getUser().getId());
boolean isAdmin = event.getMember() != null
&& event.getMember().hasPermission(Permission.ADMINISTRATOR);
boolean isSuper = config.getSuperAdminId().equals(event.getUser().getId());
commandsByCategory.keySet().stream()
.sorted()
.forEach(
cat -> {
if (!HelpCategories.isCategoryVisible(cat, isAdmin, isSuper)) {
return;
}
commandsByCategory.keySet().stream()
.sorted()
.forEach(cat -> {
if (!HelpCategories.isCategoryVisible(cat, isAdmin, isSuper)) {
return;
}
String emoji = HelpCategories.getEmoji(cat);
int count = commandsByCategory.get(cat).size();
String emoji = HelpCategories.getEmoji(cat);
int count = commandsByCategory.get(cat).size();
menuBuilder.addOption(
cat,
"category:" + cat,
count + " команд",
net.dv8tion.jda.api.entities.emoji.Emoji.fromUnicode(emoji)
);
menuBuilder.addOption(
cat,
"category:" + cat,
count + " команд",
net.dv8tion.jda.api.entities.emoji.Emoji.fromUnicode(emoji));
});
event.editMessageEmbeds(embed.build())
.setActionRow(menuBuilder.build())
.queue();
}
event.editMessageEmbeds(embed.build()).setActionRow(menuBuilder.build()).queue();
}
private EmbedFactory createCategoryEmbed(String category, List<Command> commands,
StringSelectInteractionEvent event) {
String emoji = HelpCategories.getEmoji(category);
Color color = HelpCategories.getColor(category);
private EmbedFactory createCategoryEmbed(
String category, List<Command> commands, StringSelectInteractionEvent event) {
String emoji = HelpCategories.getEmoji(category);
Color color = HelpCategories.getColor(category);
EmbedFactory embed = EmbedFactory.create()
EmbedFactory embed =
EmbedFactory.create()
.withTitle(emoji + " " + category)
.withDescription("Список команд категории **" + category + "**\n" +
"Префикс: `" + config.getPrefix() + "`")
.withDescription(
"Список команд категории **"
+ category
+ "**\n"
+ "Префикс: `"
+ config.getPrefix()
+ "`")
.withColor(color);
// Группируем команды по 8 в поле
StringBuilder commandList = new StringBuilder();
int count = 0;
// Группируем команды по 8 в поле
StringBuilder commandList = new StringBuilder();
int count = 0;
for (Command cmd : commands) {
commandList.append("`").append(config.getPrefix()).append(cmd.getUsage()).append("`\n")
.append(cmd.getDescription()).append("\n\n");
count++;
for (Command cmd : commands) {
commandList
.append("`")
.append(config.getPrefix())
.append(cmd.getUsage())
.append("`\n")
.append(cmd.getDescription())
.append("\n\n");
count++;
// Если достигли 8 команд или лимита символов, создаём новое поле
if (count % 8 == 0 || commandList.length() > 900) {
embed.addField("", commandList.toString().trim(), false);
commandList = new StringBuilder();
}
}
// Добавляем оставшиеся команды
if (commandList.length() > 0) {
embed.addField("", commandList.toString().trim(), false);
}
embed.addField("💡 Подсказка",
"Используй селектор ниже для переключения между категориями",
false);
embed.withFooter(
"Запросил: " + event.getUser().getName() + " • Всего команд: " + commands.size(),
event.getUser().getAvatarUrl()
);
return embed;
// Если достигли 8 команд или лимита символов, создаём новое поле
if (count % 8 == 0 || commandList.length() > 900) {
embed.addField("", commandList.toString().trim(), false);
commandList = new StringBuilder();
}
}
// Добавляем оставшиеся команды
if (commandList.length() > 0) {
embed.addField("", commandList.toString().trim(), false);
}
embed.addField(
"💡 Подсказка", "Используй селектор ниже для переключения между категориями", false);
embed.withFooter(
"Запросил: " + event.getUser().getName() + " • Всего команд: " + commands.size(),
event.getUser().getAvatarUrl());
return embed;
}
}

View file

@ -1,5 +1,10 @@
package creator.loki.feature.help.commands;
import java.awt.Color;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.config.BotConfig;
@ -10,119 +15,136 @@ import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.interactions.components.selections.StringSelectMenu;
import java.awt.Color;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class HelpCommand implements Command {
private final CommandRegistry registry;
private final BotConfig config;
private final CommandRegistry registry;
private final BotConfig config;
public HelpCommand(CommandRegistry registry, BotConfig config) {
this.registry = registry;
this.config = config;
}
public HelpCommand(CommandRegistry registry, BotConfig config) {
this.registry = registry;
this.config = config;
}
@Override
public String getName() {
return "help";
}
@Override
public String getName() {
return "help";
}
@Override
public String getDescription() {
return "Показывает список всех команд";
}
@Override
public String getDescription() {
return "Показывает список всех команд";
}
@Override
public void execute(CommandContext context) {
var commands = registry.getAllCommands();
Map<String, List<Command>> commandsByCategory = commands.stream()
.collect(Collectors.groupingBy(Command::getCategory));
@Override
public void execute(CommandContext context) {
var commands = registry.getAllCommands();
Map<String, List<Command>> commandsByCategory =
commands.stream().collect(Collectors.groupingBy(Command::getCategory));
User viewer = context.getAuthor() != null ? context.getAuthor().getUser() : context.getMessage().getAuthor();
boolean isAdmin = context.getAuthor() != null
&& context.getAuthor().hasPermission(Permission.ADMINISTRATOR);
boolean isSuper = config.getSuperAdminId().equals(viewer.getId());
User viewer =
context.getAuthor() != null
? context.getAuthor().getUser()
: context.getMessage().getAuthor();
boolean isAdmin =
context.getAuthor() != null && context.getAuthor().hasPermission(Permission.ADMINISTRATOR);
boolean isSuper = config.getSuperAdminId().equals(viewer.getId());
// Главная страница
EmbedFactory embed = createMainPage(context, commandsByCategory, isAdmin, isSuper);
// Главная страница
EmbedFactory embed = createMainPage(context, commandsByCategory, isAdmin, isSuper);
// Создаём селектор категорий
StringSelectMenu.Builder menuBuilder = StringSelectMenu.create("help:category")
.setPlaceholder("📂 Выберите категорию");
// Создаём селектор категорий
StringSelectMenu.Builder menuBuilder =
StringSelectMenu.create("help:category").setPlaceholder("📂 Выберите категорию");
commandsByCategory.keySet().stream()
.sorted()
.forEach(category -> {
if (!HelpCategories.isCategoryVisible(category, isAdmin, isSuper)) {
return;
}
commandsByCategory.keySet().stream()
.sorted()
.forEach(
category -> {
if (!HelpCategories.isCategoryVisible(category, isAdmin, isSuper)) {
return;
}
String emoji = HelpCategories.getEmoji(category);
String description = HelpCategories.getDescription(category);
int count = commandsByCategory.get(category).size();
String emoji = HelpCategories.getEmoji(category);
String description = HelpCategories.getDescription(category);
int count = commandsByCategory.get(category).size();
menuBuilder.addOption(
category,
"category:" + category,
description + " (" + count + " команд)",
net.dv8tion.jda.api.entities.emoji.Emoji.fromUnicode(emoji)
);
menuBuilder.addOption(
category,
"category:" + category,
description + " (" + count + " команд)",
net.dv8tion.jda.api.entities.emoji.Emoji.fromUnicode(emoji));
});
context.getChannel()
.sendMessageEmbeds(embed.build())
.addActionRow(menuBuilder.build())
.queue();
}
context.getChannel().sendMessageEmbeds(embed.build()).addActionRow(menuBuilder.build()).queue();
}
private EmbedFactory createMainPage(CommandContext context, Map<String, List<Command>> commandsByCategory,
boolean isAdmin, boolean isSuper) {
int totalCommands = commandsByCategory.entrySet().stream()
private EmbedFactory createMainPage(
CommandContext context,
Map<String, List<Command>> commandsByCategory,
boolean isAdmin,
boolean isSuper) {
int totalCommands =
commandsByCategory.entrySet().stream()
.filter(e -> HelpCategories.isCategoryVisible(e.getKey(), isAdmin, isSuper))
.mapToInt(e -> e.getValue().size())
.sum();
StringBuilder categoriesText = new StringBuilder();
commandsByCategory.keySet().stream()
.sorted()
.forEach(category -> {
if (!HelpCategories.isCategoryVisible(category, isAdmin, isSuper)) {
return;
}
StringBuilder categoriesText = new StringBuilder();
commandsByCategory.keySet().stream()
.sorted()
.forEach(
category -> {
if (!HelpCategories.isCategoryVisible(category, isAdmin, isSuper)) {
return;
}
String emoji = HelpCategories.getEmoji(category);
int count = commandsByCategory.get(category).size();
categoriesText.append(emoji).append(" **").append(category)
.append("** - ").append(count).append(" команд\n");
String emoji = HelpCategories.getEmoji(category);
int count = commandsByCategory.get(category).size();
categoriesText
.append(emoji)
.append(" **")
.append(category)
.append("** - ")
.append(count)
.append(" команд\n");
});
return EmbedFactory.create()
.withTitle("📚 Справка по командам Loki Bot")
.withDescription(
"Привет! Я многофункциональный Discord бот с системой уровней, " +
"личными голосовыми каналами и множеством развлекательных команд.\n\n" +
"**Префикс команд:** `" + config.getPrefix() + "`\n" +
"**Всего команд:** " + totalCommands + "\n\n" +
"**📂 Категории команд:**\n" + categoriesText.toString() + "\n" +
"Используй селектор ниже для просмотра команд категории."
)
.withColor(new Color(88, 101, 242))
.withThumbnail(context.getGuild() != null ? context.getGuild().getIconUrl() : null)
.addField("💡 Как использовать",
"Все команды - с префиксом `" + config.getPrefix() + "`\n" +
"Примеры: `" + config.getPrefix() + "rank`, `" + config.getPrefix() + "ship @user1 @user2`, `" +
config.getPrefix() + "voice-setup info`",
false)
.withFooter(
"Запросил: " + context.getAuthor().getUser().getName(),
context.getAuthor().getUser().getAvatarUrl()
);
}
return EmbedFactory.create()
.withTitle("📚 Справка по командам Loki Bot")
.withDescription(
"Привет! Я многофункциональный Discord бот с системой уровней, "
+ "личными голосовыми каналами и множеством развлекательных команд.\n\n"
+ "**Префикс команд:** `"
+ config.getPrefix()
+ "`\n"
+ "**Всего команд:** "
+ totalCommands
+ "\n\n"
+ "**📂 Категории команд:**\n"
+ categoriesText.toString()
+ "\n"
+ "Используй селектор ниже для просмотра команд категории.")
.withColor(new Color(88, 101, 242))
.withThumbnail(context.getGuild() != null ? context.getGuild().getIconUrl() : null)
.addField(
"💡 Как использовать",
"Все команды - с префиксом `"
+ config.getPrefix()
+ "`\n"
+ "Примеры: `"
+ config.getPrefix()
+ "rank`, `"
+ config.getPrefix()
+ "ship @user1 @user2`, `"
+ config.getPrefix()
+ "voice-setup info`",
false)
.withFooter(
"Запросил: " + context.getAuthor().getUser().getName(),
context.getAuthor().getUser().getAvatarUrl());
}
@Override
public String getCategory() {
return "Утилиты";
}
@Override
public String getCategory() {
return "Утилиты";
}
}

View file

@ -1,58 +1,60 @@
package creator.loki.feature.info.commands;
import java.awt.Color;
import java.util.List;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.User;
import java.awt.Color;
import java.util.List;
public class AvatarCommand implements Command {
@Override
public String getName() {
return "avatar";
@Override
public String getName() {
return "avatar";
}
@Override
public String getDescription() {
return "Показывает аватар пользователя";
}
@Override
public String getUsage() {
return "avatar [@user]";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
User targetUser;
// Если упомянут пользователь - показываем его аватар
List<Member> mentioned = context.getMessage().getMentions().getMembers();
if (!mentioned.isEmpty()) {
targetUser = mentioned.get(0).getUser();
} else {
// Иначе показываем аватар автора команды
targetUser = context.getAuthor().getUser();
}
@Override
public String getDescription() {
return "Показывает аватар пользователя";
}
String avatarUrl = targetUser.getEffectiveAvatarUrl() + "?size=512";
@Override
public String getUsage() {
return "avatar [@user]";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
User targetUser;
// Если упомянут пользователь - показываем его аватар
List<Member> mentioned = context.getMessage().getMentions().getMembers();
if (!mentioned.isEmpty()) {
targetUser = mentioned.get(0).getUser();
} else {
// Иначе показываем аватар автора команды
targetUser = context.getAuthor().getUser();
}
String avatarUrl = targetUser.getEffectiveAvatarUrl() + "?size=512";
context.getChannel().sendMessageEmbeds(
context
.getChannel()
.sendMessageEmbeds(
EmbedFactory.create()
.withTitle("🖼️ Аватар " + targetUser.getName())
.withImage(avatarUrl)
.withColor(Color.BLUE)
.withFooter("ID: " + targetUser.getId())
.build()
).queue();
}
.build())
.queue();
}
}

View file

@ -1,5 +1,9 @@
package creator.loki.feature.info.commands;
import java.awt.Color;
import java.time.format.DateTimeFormatter;
import java.util.stream.Collectors;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
@ -9,108 +13,107 @@ import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Role;
import net.dv8tion.jda.api.entities.User;
import java.awt.Color;
import java.time.format.DateTimeFormatter;
import java.util.stream.Collectors;
public class InfoCommand implements Command {
@Override
public String getName() {
return "info";
@Override
public String getName() {
return "info";
}
@Override
public String getDescription() {
return "Информация о сервере или пользователе";
}
@Override
public String getUsage() {
return "info [server|user]";
}
@Override
public void execute(CommandContext context) {
String type = context.getArgs().isEmpty() ? "server" : context.getArgs().get(0).toLowerCase();
switch (type) {
case "server" -> showServerInfo(context);
case "user" -> showUserInfo(context);
default ->
context
.getChannel()
.sendMessageEmbeds(
EmbedTemplates.error("Неверный аргумент", "Используй: `!info [server|user]`"))
.queue();
}
}
@Override
public String getDescription() {
return "Информация о сервере или пользователе";
}
private void showServerInfo(CommandContext context) {
Guild guild = context.getGuild();
if (guild == null) return;
@Override
public String getUsage() {
return "info [server|user]";
}
String iconUrl = guild.getIconUrl();
if (iconUrl == null) iconUrl = "";
@Override
public void execute(CommandContext context) {
String type = context.getArgs().isEmpty() ? "server" : context.getArgs().get(0).toLowerCase();
switch (type) {
case "server" -> showServerInfo(context);
case "user" -> showUserInfo(context);
default -> context.getChannel().sendMessageEmbeds(
EmbedTemplates.error(
"Неверный аргумент",
"Используй: `!info [server|user]`"
)
).queue();
}
}
private void showServerInfo(CommandContext context) {
Guild guild = context.getGuild();
if (guild == null) return;
String iconUrl = guild.getIconUrl();
if (iconUrl == null) iconUrl = "";
context.getChannel().sendMessageEmbeds(
context
.getChannel()
.sendMessageEmbeds(
EmbedFactory.create()
.withTitle("🏰 Информация о сервере")
.withColor(Color.BLUE)
.withThumbnail(iconUrl)
.addField("Название", guild.getName(), true)
.addField("ID", guild.getId(), true)
.addField("Владелец", guild.getOwner() != null ? guild.getOwner().getAsMention() : "Неизвестно", true)
.addField(
"Владелец",
guild.getOwner() != null ? guild.getOwner().getAsMention() : "Неизвестно",
true)
.addField("Участников", String.valueOf(guild.getMemberCount()), true)
.addField("Каналов", String.valueOf(guild.getChannels().size()), true)
.addField("Ролей", String.valueOf(guild.getRoles().size()), true)
.addField(
"Создан",
guild.getTimeCreated().format(DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm")),
false
)
.build()
).queue();
false)
.build())
.queue();
}
private void showUserInfo(CommandContext context) {
Member member = context.getAuthor();
if (member == null) return;
User user = member.getUser();
String roles =
member.getRoles().stream().map(Role::getAsMention).collect(Collectors.joining(", "));
if (roles.isEmpty()) {
roles = "Нет ролей";
}
private void showUserInfo(CommandContext context) {
Member member = context.getAuthor();
if (member == null) return;
User user = member.getUser();
String roles = member.getRoles().stream()
.map(Role::getAsMention)
.collect(Collectors.joining(", "));
if (roles.isEmpty()) {
roles = "Нет ролей";
}
context.getChannel().sendMessageEmbeds(
context
.getChannel()
.sendMessageEmbeds(
EmbedFactory.create()
.withTitle("👤 Информация о пользователе")
.withColor(Color.CYAN)
.withThumbnail(user.getEffectiveAvatarUrl())
.addField("Имя", user.getName(), true)
.addField("ID", user.getId(), true)
.addField("Никнейм", member.getNickname() != null ? member.getNickname() : "Не установлен", true)
.addField(
"Никнейм",
member.getNickname() != null ? member.getNickname() : "Не установлен",
true)
.addField("Бот?", user.isBot() ? "Да" : "Нет", true)
.addField(
"Присоединился",
member.getTimeJoined().format(DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm")),
false
)
.addField(
"Роли (" + member.getRoles().size() + ")",
roles,
false
)
.build()
).queue();
}
false)
.addField("Роли (" + member.getRoles().size() + ")", roles, false)
.build())
.queue();
}
@Override
public String getCategory() {
return "Утилиты";
}
@Override
public String getCategory() {
return "Утилиты";
}
}

View file

@ -1,44 +1,49 @@
package creator.loki.feature.info.commands;
import java.awt.Color;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import java.awt.Color;
public class PingCommand implements Command {
@Override
public String getName() {
return "ping";
}
@Override
public String getName() {
return "ping";
}
@Override
public String getDescription() {
return "Проверка задержки бота";
}
@Override
public String getDescription() {
return "Проверка задержки бота";
}
@Override
public void execute(CommandContext context) {
long startTime = System.currentTimeMillis();
@Override
public void execute(CommandContext context) {
long startTime = System.currentTimeMillis();
context.getChannel().sendMessage("Измеряю задержку...").queue(message -> {
long ping = System.currentTimeMillis() - startTime;
long gatewayPing = context.getMessage().getJDA().getGatewayPing();
context
.getChannel()
.sendMessage("Измеряю задержку...")
.queue(
message -> {
long ping = System.currentTimeMillis() - startTime;
long gatewayPing = context.getMessage().getJDA().getGatewayPing();
message.editMessageEmbeds(
EmbedFactory.create()
.withTitle("🏓 Pong!")
.withColor(Color.GREEN)
.addField("Задержка API", ping + "ms", true)
.addField("Задержка Gateway", gatewayPing + "ms", true)
.build()
).queue();
});
}
message
.editMessageEmbeds(
EmbedFactory.create()
.withTitle("🏓 Pong!")
.withColor(Color.GREEN)
.addField("Задержка API", ping + "ms", true)
.addField("Задержка Gateway", gatewayPing + "ms", true)
.build())
.queue();
});
}
@Override
public String getCategory() {
return "Утилиты";
}
@Override
public String getCategory() {
return "Утилиты";
}
}

View file

@ -1,128 +1,187 @@
package creator.loki.feature.info.commands;
import java.awt.Color;
import java.time.format.DateTimeFormatter;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import creator.loki.feature.serverstats.ServerStatsService;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.channel.ChannelType;
import net.dv8tion.jda.api.entities.channel.concrete.Category;
import net.dv8tion.jda.api.utils.FileUpload;
import java.awt.Color;
import java.time.format.DateTimeFormatter;
public class ServerStatsCommand implements Command {
private final ServerStatsService statsService;
private final ServerStatsService statsService;
public ServerStatsCommand(ServerStatsService statsService) {
this.statsService = statsService;
public ServerStatsCommand(ServerStatsService statsService) {
this.statsService = statsService;
}
@Override
public String getName() {
return "serverstats";
}
@Override
public String getDescription() {
return "📊 Статистика сервера (участники, каналы, роли)";
}
@Override
public String getUsage() {
return "serverstats [csv|chart]";
}
@Override
public String getCategory() {
return "Информация";
}
@Override
public void execute(CommandContext context) {
Guild guild = context.getGuild();
if (guild == null) {
context.getChannel().sendMessage("Только на сервере.").queue();
return;
}
@Override
public String getName() {
return "serverstats";
String arg = context.getArgs().isEmpty() ? "" : context.getArgs().get(0).toLowerCase();
switch (arg) {
case "csv" -> handleCsv(context, guild);
case "chart" -> handleChart(context, guild);
default -> handleEmbed(context, guild);
}
}
@Override
public String getDescription() {
return "📊 Статистика сервера (участники, каналы, роли)";
}
private void handleEmbed(CommandContext context, Guild guild) {
guild
.loadMembers()
.onSuccess(
members -> {
long total = members.size();
long bots = members.stream().filter(m -> m.getUser().isBot()).count();
long humans = total - bots;
long online =
members.stream()
.filter(
m ->
m.getOnlineStatus() != null
&& m.getOnlineStatus() != net.dv8tion.jda.api.OnlineStatus.OFFLINE
&& m.getOnlineStatus()
!= net.dv8tion.jda.api.OnlineStatus.UNKNOWN)
.count();
@Override
public String getUsage() {
return "serverstats [csv|chart]";
}
long textChannels = guild.getTextChannels().size();
long voiceChannels = guild.getVoiceChannels().size();
long categories = guild.getCategories().size();
long totalChannels = textChannels + voiceChannels + categories;
@Override
public String getCategory() {
return "Информация";
}
int roles = guild.getRoles().size();
int emojis = guild.getEmojis().size();
int boostTier = guild.getBoostTier().getKey();
int boostCount = guild.getBoostCount();
@Override
public void execute(CommandContext context) {
Guild guild = context.getGuild();
if (guild == null) {
context.getChannel().sendMessage("Только на сервере.").queue();
return;
}
String created =
guild.getTimeCreated().format(DateTimeFormatter.ofPattern("dd.MM.yyyy"));
String arg = context.getArgs().isEmpty() ? "" : context.getArgs().get(0).toLowerCase();
context
.getChannel()
.sendMessageEmbeds(
EmbedFactory.create()
.withTitle("📊 Статистика сервера")
.withThumbnail(guild.getIconUrl())
.withColor(new Color(88, 101, 242))
.addField(
"👤 Участники",
"Всего: **"
+ total
+ "**\n👥 Людей: **"
+ humans
+ "**\n🤖 Ботов: **"
+ bots
+ "**\n🟢 Онлайн: **"
+ online
+ "**",
true)
.addField(
"💬 Каналы",
"Текстовых: **"
+ textChannels
+ "**\n"
+ "🔊 Голосовых: **"
+ voiceChannels
+ "**\n"
+ "📁 Категорий: **"
+ categories
+ "**\n"
+ "Всего: **"
+ totalChannels
+ "**",
true)
.addField(
"⚙️ Прочее",
"Ролей: **"
+ roles
+ "**\n😀 Эмодзи: **"
+ emojis
+ "**\n🚀 Бустов: **"
+ boostCount
+ "** (ур. "
+ boostTier
+ ")",
true)
.withFooter(
"Создан: "
+ created
+ " | Владелец: "
+ (guild.getOwner() != null
? guild.getOwner().getUser().getGlobalName()
: "неизвестен"))
.build())
.queue();
});
}
switch (arg) {
case "csv" -> handleCsv(context, guild);
case "chart" -> handleChart(context, guild);
default -> handleEmbed(context, guild);
}
}
private void handleEmbed(CommandContext context, Guild guild) {
guild.loadMembers().onSuccess(members -> {
long total = members.size();
long bots = members.stream().filter(m -> m.getUser().isBot()).count();
long humans = total - bots;
long online = members.stream().filter(m -> m.getOnlineStatus() != null
&& m.getOnlineStatus() != net.dv8tion.jda.api.OnlineStatus.OFFLINE
&& m.getOnlineStatus() != net.dv8tion.jda.api.OnlineStatus.UNKNOWN).count();
long textChannels = guild.getTextChannels().size();
long voiceChannels = guild.getVoiceChannels().size();
long categories = guild.getCategories().size();
long totalChannels = textChannels + voiceChannels + categories;
int roles = guild.getRoles().size();
int emojis = guild.getEmojis().size();
int boostTier = guild.getBoostTier().getKey();
int boostCount = guild.getBoostCount();
String created = guild.getTimeCreated().format(DateTimeFormatter.ofPattern("dd.MM.yyyy"));
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.withTitle("📊 Статистика сервера")
.withThumbnail(guild.getIconUrl())
.withColor(new Color(88, 101, 242))
.addField("👤 Участники",
"Всего: **" + total + "**\n👥 Людей: **" + humans + "**\n🤖 Ботов: **" + bots + "**\n🟢 Онлайн: **" + online + "**", true)
.addField("💬 Каналы",
"Текстовых: **" + textChannels + "**\n"
+ "🔊 Голосовых: **" + voiceChannels + "**\n"
+ "📁 Категорий: **" + categories + "**\n"
+ "Всего: **" + totalChannels + "**", true)
.addField("⚙️ Прочее",
"Ролей: **" + roles + "**\n😀 Эмодзи: **" + emojis + "**\n🚀 Бустов: **" + boostCount + "** (ур. " + boostTier + ")", true)
.withFooter("Создан: " + created + " | Владелец: "
+ (guild.getOwner() != null ? guild.getOwner().getUser().getGlobalName() : "неизвестен"))
.build()
).queue();
});
}
private void handleCsv(CommandContext context, Guild guild) {
context.getChannel().sendMessage("⏳ Генерирую CSV...").queue(msg -> {
try {
private void handleCsv(CommandContext context, Guild guild) {
context
.getChannel()
.sendMessage("⏳ Генерирую CSV...")
.queue(
msg -> {
try {
var history = statsService.getHistory(30);
byte[] csv = statsService.generateCsv(guild, history);
context.getChannel().sendFiles(
FileUpload.fromData(csv, "serverstats.csv")
).queue(s -> msg.delete().queue(), e -> msg.editMessage("❌ Ошибка генерации CSV").queue());
} catch (Exception e) {
context
.getChannel()
.sendFiles(FileUpload.fromData(csv, "serverstats.csv"))
.queue(
s -> msg.delete().queue(),
e -> msg.editMessage("❌ Ошибка генерации CSV").queue());
} catch (Exception e) {
msg.editMessage("❌ Ошибка: " + e.getMessage()).queue();
}
});
}
}
});
}
private void handleChart(CommandContext context, Guild guild) {
context.getChannel().sendMessage("⏳ Генерирую график...").queue(msg -> {
try {
private void handleChart(CommandContext context, Guild guild) {
context
.getChannel()
.sendMessage("⏳ Генерирую график...")
.queue(
msg -> {
try {
var history = statsService.getHistory(30);
byte[] chart = statsService.generateChart(history);
context.getChannel().sendFiles(
FileUpload.fromData(chart, "chart.png")
).queue(s -> msg.delete().queue(), e -> msg.editMessage("❌ Ошибка отправки графика").queue());
} catch (Exception e) {
context
.getChannel()
.sendFiles(FileUpload.fromData(chart, "chart.png"))
.queue(
s -> msg.delete().queue(),
e -> msg.editMessage("❌ Ошибка отправки графика").queue());
} catch (Exception e) {
msg.editMessage("❌ Ошибка: " + e.getMessage()).queue();
}
});
}
}
});
}
}

View file

@ -6,50 +6,51 @@ import creator.loki.feature.level.service.LeaderboardService;
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
/**
* Обработчик кнопок таблицы лидеров (Single Responsibility)
*/
/** Обработчик кнопок таблицы лидеров (Single Responsibility) */
@Singleton
public class LeaderboardButtonListener extends ListenerAdapter {
private final LeaderboardService leaderboardService;
private final LeaderboardService leaderboardService;
@Inject
public LeaderboardButtonListener(LeaderboardService leaderboardService) {
this.leaderboardService = leaderboardService;
@Inject
public LeaderboardButtonListener(LeaderboardService leaderboardService) {
this.leaderboardService = leaderboardService;
}
@Override
public void onButtonInteraction(ButtonInteractionEvent event) {
String buttonId = event.getComponentId();
if (!buttonId.startsWith("lb_")) {
return;
}
@Override
public void onButtonInteraction(ButtonInteractionEvent event) {
String buttonId = event.getComponentId();
event.deferEdit().queue();
if (!buttonId.startsWith("lb_")) {
return;
}
event.deferEdit().queue();
switch (buttonId) {
case "lb_level" -> showLevelLeaderboard(event);
case "lb_messages" -> showMessageLeaderboard(event);
case "lb_voice" -> showVoiceLeaderboard(event);
}
switch (buttonId) {
case "lb_level" -> showLevelLeaderboard(event);
case "lb_messages" -> showMessageLeaderboard(event);
case "lb_voice" -> showVoiceLeaderboard(event);
default -> {
/* ignore */
}
}
}
private void showLevelLeaderboard(ButtonInteractionEvent event) {
var top = leaderboardService.getTopByLevel(10);
var embed = LeaderboardFormatter.formatLevelLeaderboard(top);
event.getHook().editOriginalEmbeds(embed).queue();
}
private void showLevelLeaderboard(ButtonInteractionEvent event) {
var top = leaderboardService.getTopByLevel(10);
var embed = LeaderboardFormatter.formatLevelLeaderboard(top);
event.getHook().editOriginalEmbeds(embed).queue();
}
private void showMessageLeaderboard(ButtonInteractionEvent event) {
var top = leaderboardService.getTopByMessages(10);
var embed = LeaderboardFormatter.formatMessageLeaderboard(top);
event.getHook().editOriginalEmbeds(embed).queue();
}
private void showMessageLeaderboard(ButtonInteractionEvent event) {
var top = leaderboardService.getTopByMessages(10);
var embed = LeaderboardFormatter.formatMessageLeaderboard(top);
event.getHook().editOriginalEmbeds(embed).queue();
}
private void showVoiceLeaderboard(ButtonInteractionEvent event) {
var top = leaderboardService.getTopByVoice(10);
var embed = LeaderboardFormatter.formatVoiceLeaderboard(top);
event.getHook().editOriginalEmbeds(embed).queue();
}
private void showVoiceLeaderboard(ButtonInteractionEvent event) {
var top = leaderboardService.getTopByVoice(10);
var embed = LeaderboardFormatter.formatVoiceLeaderboard(top);
event.getHook().editOriginalEmbeds(embed).queue();
}
}

View file

@ -1,19 +1,16 @@
package creator.loki.feature.leaderboard;
import net.dv8tion.jda.api.interactions.components.buttons.Button;
import java.util.List;
/**
* Кнопки для таблицы лидеров
*/
import net.dv8tion.jda.api.interactions.components.buttons.Button;
/** Кнопки для таблицы лидеров */
public class LeaderboardButtons {
public static List<Button> getButtons() {
return List.of(
Button.primary("lb_level", "📊 Уровни"),
Button.success("lb_messages", "💬 Сообщения"),
Button.secondary("lb_voice", "🎤 Войс")
);
}
public static List<Button> getButtons() {
return List.of(
Button.primary("lb_level", "📊 Уровни"),
Button.success("lb_messages", "💬 Сообщения"),
Button.secondary("lb_voice", "🎤 Войс"));
}
}

View file

@ -1,58 +1,59 @@
package creator.loki.feature.leaderboard;
import java.util.concurrent.TimeUnit;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.feature.level.service.LeaderboardService;
import java.util.concurrent.TimeUnit;
/**
* Команда таблицы лидеров (рефакторинг: 241 → 50 строк)
*/
/** Команда таблицы лидеров (рефакторинг: 241 → 50 строк) */
public class LeaderboardCommand implements Command {
private final LeaderboardService leaderboardService;
private final LeaderboardService leaderboardService;
public LeaderboardCommand(LeaderboardService leaderboardService) {
this.leaderboardService = leaderboardService;
public LeaderboardCommand(LeaderboardService leaderboardService) {
this.leaderboardService = leaderboardService;
}
@Override
public String getName() {
return "leaderboard";
}
@Override
public String getDescription() {
return "Таблица лидеров (уровни, сообщения, войс)";
}
@Override
public String getUsage() {
return "leaderboard";
}
@Override
public String getCategory() {
return "Статистика";
}
@Override
public void execute(CommandContext context) {
var top = leaderboardService.getTopByLevel(10);
if (top.isEmpty()) {
context.getChannel().sendMessage("❌ Пока нет статистики!").queue();
return;
}
@Override
public String getName() {
return "leaderboard";
}
var embed = LeaderboardFormatter.formatLevelLeaderboard(top);
var buttons = LeaderboardButtons.getButtons();
@Override
public String getDescription() {
return "Таблица лидеров (уровни, сообщения, войс)";
}
@Override
public String getUsage() {
return "leaderboard";
}
@Override
public String getCategory() {
return "Статистика";
}
@Override
public void execute(CommandContext context) {
var top = leaderboardService.getTopByLevel(10);
if (top.isEmpty()) {
context.getChannel().sendMessage("❌ Пока нет статистики!").queue();
return;
}
var embed = LeaderboardFormatter.formatLevelLeaderboard(top);
var buttons = LeaderboardButtons.getButtons();
context.getChannel().sendMessageEmbeds(embed)
.setActionRow(buttons)
.queue(message -> {
// Удаляем кнопки через 5 минут
message.editMessageComponents().queueAfter(5, TimeUnit.MINUTES);
context
.getChannel()
.sendMessageEmbeds(embed)
.setActionRow(buttons)
.queue(
message -> {
// Удаляем кнопки через 5 минут
message.editMessageComponents().queueAfter(5, TimeUnit.MINUTES);
});
}
}
}

View file

@ -1,89 +1,90 @@
package creator.loki.feature.leaderboard;
import creator.loki.embed.EmbedFactory;
import creator.loki.feature.level.domain.UserStats;
import net.dv8tion.jda.api.entities.MessageEmbed;
import java.awt.Color;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
/**
* Форматирование таблиц лидеров (DRY принцип)
*/
import creator.loki.embed.EmbedFactory;
import creator.loki.feature.level.domain.UserStats;
import net.dv8tion.jda.api.entities.MessageEmbed;
/** Форматирование таблиц лидеров (DRY принцип) */
public class LeaderboardFormatter {
public static MessageEmbed formatLevelLeaderboard(List<Map.Entry<String, UserStats>> top) {
return formatLeaderboard(
top,
"🏆 Топ-10 по уровням",
new Color(255, 215, 0),
entry -> {
UserStats stats = entry.getValue();
return "Уровень **" + stats.getLevel() + "** (" + stats.getTotalXP() + " XP)";
}
);
public static MessageEmbed formatLevelLeaderboard(List<Map.Entry<String, UserStats>> top) {
return formatLeaderboard(
top,
"🏆 Топ-10 по уровням",
new Color(255, 215, 0),
entry -> {
UserStats stats = entry.getValue();
return "Уровень **" + stats.getLevel() + "** (" + stats.getTotalXP() + " XP)";
});
}
public static MessageEmbed formatMessageLeaderboard(List<Map.Entry<String, UserStats>> top) {
return formatLeaderboard(
top,
"💬 Топ-10 по сообщениям",
new Color(52, 152, 219),
entry -> "**" + entry.getValue().getMessageCount() + "** сообщений");
}
public static MessageEmbed formatVoiceLeaderboard(List<Map.Entry<String, UserStats>> top) {
return formatLeaderboard(
top,
"🎤 Топ-10 по времени в войсе",
new Color(155, 89, 182),
entry -> {
long minutes = entry.getValue().getVoiceMinutes();
long hours = minutes / 60;
long mins = minutes % 60;
return "**" + hours + "ч " + mins + "м** в войсе";
});
}
private static MessageEmbed formatLeaderboard(
List<Map.Entry<String, UserStats>> top,
String title,
Color color,
Function<Map.Entry<String, UserStats>, String> statFormatter) {
StringBuilder description = new StringBuilder();
int position = 1;
for (var entry : top) {
String emoji = getPositionEmoji(position);
String userId = entry.getKey();
String stats = statFormatter.apply(entry);
description
.append(emoji)
.append(" **#")
.append(position)
.append("** <@")
.append(userId)
.append(">")
.append(" - ")
.append(stats)
.append("\n");
position++;
}
public static MessageEmbed formatMessageLeaderboard(List<Map.Entry<String, UserStats>> top) {
return formatLeaderboard(
top,
"💬 Топ-10 по сообщениям",
new Color(52, 152, 219),
entry -> "**" + entry.getValue().getMessageCount() + "** сообщений"
);
}
return EmbedFactory.create()
.withTitle(title)
.withDescription(description.toString())
.withColor(color)
.withFooter("Выбери категорию ниже")
.build();
}
public static MessageEmbed formatVoiceLeaderboard(List<Map.Entry<String, UserStats>> top) {
return formatLeaderboard(
top,
"🎤 Топ-10 по времени в войсе",
new Color(155, 89, 182),
entry -> {
long minutes = entry.getValue().getVoiceMinutes();
long hours = minutes / 60;
long mins = minutes % 60;
return "**" + hours + "ч " + mins + "м** в войсе";
}
);
}
private static MessageEmbed formatLeaderboard(
List<Map.Entry<String, UserStats>> top,
String title,
Color color,
Function<Map.Entry<String, UserStats>, String> statFormatter
) {
StringBuilder description = new StringBuilder();
int position = 1;
for (var entry : top) {
String emoji = getPositionEmoji(position);
String userId = entry.getKey();
String stats = statFormatter.apply(entry);
description.append(emoji)
.append(" **#").append(position).append("** <@").append(userId).append(">")
.append(" - ").append(stats).append("\n");
position++;
}
return EmbedFactory.create()
.withTitle(title)
.withDescription(description.toString())
.withColor(color)
.withFooter("Выбери категорию ниже")
.build();
}
private static String getPositionEmoji(int position) {
return switch (position) {
case 1 -> "🥇";
case 2 -> "🥈";
case 3 -> "🥉";
default -> "⭐";
};
}
private static String getPositionEmoji(int position) {
return switch (position) {
case 1 -> "🥇";
case 2 -> "🥈";
case 3 -> "🥉";
default -> "⭐";
};
}
}

View file

@ -7,38 +7,36 @@ import creator.loki.feature.config.repository.ConfigRepository;
import creator.loki.feature.leaderboard.LeaderboardCommand;
import creator.loki.feature.level.commands.LevelRoleCommand;
import creator.loki.feature.level.service.LeaderboardService;
import creator.loki.feature.level.service.LevelService;
import creator.loki.feature.profile.ProfileCommand;
import creator.loki.feature.profile.ProfileService;
public class LevelCommandRegistrar implements CommandRegistrar {
private final LeaderboardService leaderboardService;
private final ResetLevelsCommand resetLevelsCommand;
private final ProfileService profileService;
private final LevelRoleService levelRoleService;
private final ConfigRepository configRepository;
private final LeaderboardService leaderboardService;
private final ResetLevelsCommand resetLevelsCommand;
private final ProfileService profileService;
private final LevelRoleService levelRoleService;
private final ConfigRepository configRepository;
@Inject
public LevelCommandRegistrar(
LeaderboardService leaderboardService,
ResetLevelsCommand resetLevelsCommand,
ProfileService profileService,
LevelRoleService levelRoleService,
ConfigRepository configRepository
) {
this.leaderboardService = leaderboardService;
this.resetLevelsCommand = resetLevelsCommand;
this.profileService = profileService;
this.levelRoleService = levelRoleService;
this.configRepository = configRepository;
}
@Inject
public LevelCommandRegistrar(
LeaderboardService leaderboardService,
ResetLevelsCommand resetLevelsCommand,
ProfileService profileService,
LevelRoleService levelRoleService,
ConfigRepository configRepository) {
this.leaderboardService = leaderboardService;
this.resetLevelsCommand = resetLevelsCommand;
this.profileService = profileService;
this.levelRoleService = levelRoleService;
this.configRepository = configRepository;
}
@Override
public void register(CommandRegistry registry) {
registry.register(new LeaderboardCommand(leaderboardService));
registry.register(new StatsCommand());
registry.register(resetLevelsCommand);
registry.register(new ProfileCommand(profileService));
registry.register(new LevelRoleCommand(levelRoleService, configRepository));
}
@Override
public void register(CommandRegistry registry) {
registry.register(new LeaderboardCommand(leaderboardService));
registry.register(new StatsCommand());
registry.register(resetLevelsCommand);
registry.register(new ProfileCommand(profileService));
registry.register(new LevelRoleCommand(levelRoleService, configRepository));
}
}

View file

@ -5,74 +5,70 @@ import com.google.inject.Provides;
import com.google.inject.Singleton;
import creator.loki.database.Database;
import creator.loki.feature.config.repository.ConfigRepository;
import creator.loki.feature.level.repository.SqlUserStatsRepository;
import creator.loki.feature.config.repository.GuildConfigRepository;
import creator.loki.feature.level.domain.XpFormula;
import creator.loki.feature.level.repository.SqlUserStatsRepository;
import creator.loki.feature.level.repository.UserStatsRepository;
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.config.repository.GuildConfigRepository;
import creator.loki.feature.level.repository.UserStatsRepository;
public class LevelModule extends AbstractModule {
@Override
protected void configure() {
bind(VoiceTracker.class).in(Singleton.class);
bind(ResetLevelsButtonHandler.class).in(Singleton.class);
}
@Override
protected void configure() {
bind(VoiceTracker.class).in(Singleton.class);
bind(ResetLevelsButtonHandler.class).in(Singleton.class);
}
@Provides
@Singleton
UserStatsRepository provideUserStatsRepository(Database database) {
return new SqlUserStatsRepository(database);
}
@Provides
@Singleton
UserStatsRepository provideUserStatsRepository(Database database) {
return new SqlUserStatsRepository(database);
}
@Provides
@Singleton
XpFormula provideXpFormula() {
return new XpFormula();
}
@Provides
@Singleton
XpFormula provideXpFormula() {
return new XpFormula();
}
@Provides
@Singleton
MessageCooldownService provideMessageCooldownService(UserStatsRepository repository) {
return new MessageCooldownService(repository);
}
@Provides
@Singleton
MessageCooldownService provideMessageCooldownService(UserStatsRepository repository) {
return new MessageCooldownService(repository);
}
@Provides
@Singleton
LeaderboardService provideLeaderboardService(UserStatsRepository repository) {
return new LeaderboardService(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
LevelService provideLevelService(
UserStatsRepository repository, XpFormula xpFormula, MessageCooldownService cooldownService) {
return new LevelService(repository, xpFormula, cooldownService);
}
@Provides
@Singleton
LevelRoleService provideLevelRoleService(ConfigRepository configRepository) {
return new LevelRoleService(configRepository);
}
@Provides
@Singleton
LevelRoleService provideLevelRoleService(ConfigRepository configRepository) {
return new LevelRoleService(configRepository);
}
@Provides
@Singleton
LevelUpNotifier provideLevelUpNotifier(ConfigRepository configRepo, LevelService levelService, LevelRoleService levelRoleService) {
return new LevelUpNotifier(configRepo, levelService, levelRoleService);
}
@Provides
@Singleton
LevelUpNotifier provideLevelUpNotifier(
ConfigRepository configRepo, LevelService levelService, LevelRoleService levelRoleService) {
return new LevelUpNotifier(configRepo, levelService, levelRoleService);
}
@Provides
@Singleton
ResetLevelsCommand provideResetLevelsCommand(
UserStatsRepository userStatsRepository,
GuildConfigRepository guildConfigRepository
) {
return new ResetLevelsCommand(userStatsRepository, guildConfigRepository);
}
@Provides
@Singleton
ResetLevelsCommand provideResetLevelsCommand(
UserStatsRepository userStatsRepository, GuildConfigRepository guildConfigRepository) {
return new ResetLevelsCommand(userStatsRepository, guildConfigRepository);
}
}

View file

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

View file

@ -1,93 +1,101 @@
package creator.loki.feature.level;
import java.awt.Color;
import creator.loki.embed.EmbedFactory;
import creator.loki.feature.config.repository.ConfigRepository;
import creator.loki.feature.level.domain.LevelProgress;
import creator.loki.feature.level.service.LevelService;
import creator.loki.util.LoggerDecorator;
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;
private final LevelRoleService levelRoleService;
private static final Logger logger = LoggerFactory.getLogger(LevelUpNotifier.class);
private final ConfigRepository configRepo;
private final LevelService levelService;
private final LevelRoleService levelRoleService;
public LevelUpNotifier(ConfigRepository configRepo, LevelService levelService, LevelRoleService levelRoleService) {
this.configRepo = configRepo;
this.levelService = levelService;
this.levelRoleService = levelRoleService;
}
public LevelUpNotifier(
ConfigRepository configRepo, LevelService levelService, LevelRoleService levelRoleService) {
this.configRepo = configRepo;
this.levelService = levelService;
this.levelRoleService = levelRoleService;
}
/**
* Отправить уведомление о повышении уровня
*/
public void sendLevelUpNotification(Member member, int oldLevel, int newLevel, MessageChannel channel) {
LevelProgress progress = levelService.getProgress(member.getId());
/** Отправить уведомление о повышении уровня */
public void sendLevelUpNotification(
Member member, int oldLevel, int newLevel, MessageChannel channel) {
LevelProgress progress = levelService.getProgress(member.getId());
String progressBar = buildProgressBar(progress.getPercentage());
String rewardRole = levelRoleService.assignRewardRole(member, newLevel);
String progressBar = buildProgressBar(progress.getPercentage());
String rewardRole = levelRoleService.assignRewardRole(member, newLevel);
EmbedFactory embed = EmbedFactory.create()
EmbedFactory embed =
EmbedFactory.create()
.withTitle("🎉 Повышение уровня!")
.withDescription(member.getAsMention() + " достиг **" + newLevel + " уровня**!")
.addField("Прогресс", progressBar, false)
.addField("Всего XP", String.valueOf(progress.getTotalXP()), true)
.addField("До следующего", progress.getCurrentLevelXP() + " / " + progress.getXpForNextLevel() + " XP", true)
.addField(
"До следующего",
progress.getCurrentLevelXP() + " / " + progress.getXpForNextLevel() + " XP",
true)
.withColor(getColorForLevel(newLevel))
.withThumbnail(member.getEffectiveAvatarUrl())
.withFooter("Продолжай в том же духе!");
if (rewardRole != null) {
embed.addField("🎁 Награда", rewardRole, false);
}
channel.sendMessageEmbeds(embed.build()).queue(
success -> logger.info("Отправлено уведомление о level up для {}: {} -> {}",
member.getEffectiveName(), oldLevel, newLevel),
error -> logger.error("Ошибка отправки уведомления о level up", error)
);
if (rewardRole != null) {
embed.addField("🎁 Награда", rewardRole, false);
}
/**
* Отправить уведомление о level up в войсе
*/
public void sendVoiceLevelUpNotification(Member member, int oldLevel, int newLevel) {
String channelId = configRepo.get("levelup.notification_channel_id");
if (channelId == null) {
// Если канал не настроен, пропускаем
return;
}
channel
.sendMessageEmbeds(embed.build())
.queue(
success ->
LoggerDecorator.bot(
logger,
"Отправлено уведомление о level up для "
+ member.getEffectiveName()
+ ": "
+ oldLevel
+ " -> "
+ newLevel),
error ->
LoggerDecorator.error(logger, "Ошибка отправки уведомления о level up", error));
}
TextChannel channel = member.getGuild().getTextChannelById(channelId);
if (channel != null) {
sendLevelUpNotification(member, oldLevel, newLevel, channel);
}
/** Отправить уведомление о level up в войсе */
public void sendVoiceLevelUpNotification(Member member, int oldLevel, int newLevel) {
String channelId = configRepo.get("levelup.notification_channel_id");
if (channelId == null) {
// Если канал не настроен, пропускаем
return;
}
/**
* Получить цвет для уровня
*/
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"); // Зеленый
TextChannel channel = member.getGuild().getTextChannelById(channelId);
if (channel != null) {
sendLevelUpNotification(member, oldLevel, newLevel, channel);
}
}
private String buildProgressBar(double percentage) {
int filled = (int) (percentage / 10);
filled = Math.min(filled, 10);
int empty = 10 - filled;
return "▓".repeat(filled) + "░".repeat(empty) + " " + String.format("%.0f%%", percentage);
}
/** Получить цвет для уровня */
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 buildProgressBar(double percentage) {
int filled = (int) (percentage / 10);
filled = Math.min(filled, 10);
int empty = 10 - filled;
return "▓".repeat(filled) + "░".repeat(empty) + " " + String.format("%.0f%%", percentage);
}
}

View file

@ -1,53 +1,61 @@
package creator.loki.feature.level;
import java.util.concurrent.ThreadLocalRandom;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import creator.loki.core.EventDispatcher;
import creator.loki.feature.economy.service.EconomyService;
import creator.loki.feature.level.service.LevelService;
import creator.loki.feature.level.domain.LevelUpResult;
import creator.loki.embed.EmbedTemplates;
import creator.loki.feature.economy.service.EconomyService;
import creator.loki.feature.level.domain.LevelUpResult;
import creator.loki.feature.level.service.LevelService;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import java.util.concurrent.ThreadLocalRandom;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Singleton
public class MessageListener extends ListenerAdapter {
private final EventDispatcher dispatcher;
private final LevelService levelService;
private final EconomyService economyService;
private static final Logger log = LoggerFactory.getLogger(MessageListener.class);
private final EventDispatcher dispatcher;
private final LevelService levelService;
private final EconomyService economyService;
@Inject
public MessageListener(EventDispatcher dispatcher, LevelService levelService, EconomyService economyService) {
this.dispatcher = dispatcher;
this.levelService = levelService;
this.economyService = economyService;
}
@Inject
public MessageListener(
EventDispatcher dispatcher, LevelService levelService, EconomyService economyService) {
this.dispatcher = dispatcher;
this.levelService = levelService;
this.economyService = economyService;
}
@Override
public void onMessageReceived(MessageReceivedEvent event) {
// Игнорируем ботов
if (!event.getAuthor().isBot()) {
String userId = event.getAuthor().getId();
// Начисляем XP и проверяем повышение уровня
LevelUpResult levelUp = levelService.addMessageXP(userId);
if (levelUp != null) {
// Отправляем сообщение о повышении уровня
event.getChannel().sendMessageEmbeds(
EmbedTemplates.success(
"🎉 Повышение уровня!",
event.getAuthor().getAsMention() + " достиг **" + levelUp.getNewLevel() + " уровня**!\n" +
"Всего XP: **" + levelUp.getTotalXP() + "**"
)
).queue();
}
int lcAmount = ThreadLocalRandom.current().nextInt(1, 4);
economyService.earnCash(userId, lcAmount);
@Override
public void onMessageReceived(MessageReceivedEvent event) {
try {
if (!event.getAuthor().isBot()) {
String userId = event.getAuthor().getId();
LevelUpResult levelUp = levelService.addMessageXP(userId);
if (levelUp != null) {
event
.getChannel()
.sendMessageEmbeds(
EmbedTemplates.success(
"🎉 Повышение уровня!",
event.getAuthor().getAsMention()
+ " достиг **"
+ levelUp.getNewLevel()
+ " уровня**! Всего XP: **"
+ levelUp.getTotalXP()
+ "**"))
.queue();
}
dispatcher.handleMessage(event);
int lcAmount = ThreadLocalRandom.current().nextInt(1, 4);
economyService.earnCash(userId, lcAmount);
}
} catch (Exception e) {
log.warn("XP/economy error (non-fatal): {}", e.getMessage());
}
dispatcher.handleMessage(event);
}
}

Some files were not shown because too many files have changed in this diff Show more