import { ChannelType, ChatInputCommandInteraction, EmbedBuilder, SlashCommandBuilder, } from 'discord.js'; import { prisma } from '../database'; import { SupportedLocale, t } from '../i18n'; import { FishingService } from '../services/FishingService'; export default { data: new SlashCommandBuilder() .setName('fishing') .setDescription('Play the fishing mini-game.') .setDescriptionLocalizations({ ko: '낚시 미니게임을 플레이합니다.', }) .addSubcommand((subcommand) => subcommand .setName('enter') .setDescription('Create or reopen your fishing thread.') .setDescriptionLocalizations({ ko: '낚시 전용 스레드를 생성하거나 다시 엽니다.', }), ) .addSubcommand((subcommand) => subcommand .setName('cast') .setDescription('Start a fishing session inside your fishing thread.') .setDescriptionLocalizations({ ko: '자신의 낚시 스레드 안에서 낚시 세션을 시작합니다.', }), ) .addSubcommand((subcommand) => subcommand .setName('end') .setDescription('End your active fishing session and delete the thread.') .setDescriptionLocalizations({ ko: '진행 중인 낚시 세션을 종료하고 스레드를 삭제합니다.', }), ) .addSubcommand((subcommand) => subcommand .setName('status') .setDescription('View fishing statistics.') .setDescriptionLocalizations({ ko: '낚시 통계를 확인합니다.', }) .addUserOption((option) => option .setName('user') .setDescription('User to view') .setDescriptionLocalizations({ ko: '조회할 유저', }), ), ) .addSubcommand((subcommand) => subcommand .setName('dex') .setDescription('View your fishing collection book.') .setDescriptionLocalizations({ ko: '낚시 도감을 확인합니다.', }) .addUserOption((option) => option .setName('user') .setDescription('User to view') .setDescriptionLocalizations({ ko: '조회할 유저', }), ), ) .addSubcommand((subcommand) => subcommand .setName('ranking') .setDescription('View the biggest fish size ranking in this server.') .setDescriptionLocalizations({ ko: '이 서버의 물고기 크기 랭킹을 확인합니다.', }), ), async execute(interaction: ChatInputCommandInteraction, locale: SupportedLocale) { if (!interaction.guildId) return; const config = await prisma.miniGameConfig.findUnique({ where: { guildId_gameKey: { guildId: interaction.guildId, gameKey: 'fishing' } }, }); const subcommand = interaction.options.getSubcommand(); if (subcommand === 'enter') { if (!config || !config.enabled) { await interaction.editReply({ content: t(locale, 'commands.fishing.disabled'), }); return; } if (config.channelId && config.channelId !== interaction.channelId) { await interaction.editReply({ content: t(locale, 'commands.fishing.restrictedChannel', { channel: `<#${config.channelId}>`, }), }); return; } if (!interaction.channel || interaction.channel.type !== ChannelType.GuildText) { await interaction.editReply({ content: t(locale, 'commands.fishing.enterTextChannelOnly'), }); return; } const result = await FishingService.enterThread(interaction); await interaction.editReply({ content: result.existed ? t(locale, 'commands.fishing.enterExistingThread', { thread: `<#${result.thread.id}>` }) : t(locale, 'commands.fishing.enterCreated', { thread: `<#${result.thread.id}>` }), }); return; } if (subcommand === 'cast') { if (!config || !config.enabled) { await interaction.editReply({ content: t(locale, 'commands.fishing.disabled'), }); return; } if (!FishingService.isOwnedFishingThread(interaction.channel, interaction.user.username)) { await interaction.editReply({ content: t(locale, 'commands.fishing.castThreadOnly'), }); return; } const result = await FishingService.startSessionInThread(interaction, locale); await interaction.editReply({ content: result.existed ? t(locale, 'commands.fishing.startExistingSession', { thread: `<#${result.thread.id}>` }) : t(locale, 'commands.fishing.startCreated', { thread: `<#${result.thread.id}>` }), }); return; } if (subcommand === 'end') { const ended = await FishingService.endThreadByUser(interaction, locale); if (!ended) { await interaction.editReply({ content: t(locale, 'commands.fishing.noActiveSession'), }); return; } await interaction.editReply({ content: t(locale, 'commands.fishing.endDeleted'), }); return; } if (subcommand === 'status') { const targetUser = interaction.options.getUser('user') ?? interaction.user; const profile = await FishingService.getProfile(targetUser.id, interaction.guildId); const totalCasts = profile?.totalCastCount ?? 0; const successCount = profile?.successCount ?? 0; const failCount = profile?.failCount ?? 0; const totalGoldEarned = profile?.totalGoldEarned ?? 0; const bestCatchReward = profile?.bestCatchReward ?? 0; const successRate = totalCasts > 0 ? ((successCount / totalCasts) * 100).toFixed(1) : '0.0'; const rarityBreakdown = [ `⚪ ${profile?.commonCatchCount ?? 0}`, `🟢 ${profile?.uncommonCatchCount ?? 0}`, `🔵 ${profile?.rareCatchCount ?? 0}`, `🟣 ${profile?.epicCatchCount ?? 0}`, `🟠 ${profile?.legendaryCatchCount ?? 0}`, ].join('\n'); const embed = new EmbedBuilder() .setColor(0x3b82f6) .setTitle(t(locale, 'commands.fishing.profileTitle', { user: targetUser.username })) .setThumbnail(targetUser.displayAvatarURL()) .addFields( { name: t(locale, 'commands.fishing.totalCasts'), value: String(totalCasts), inline: true, }, { name: t(locale, 'commands.fishing.successRate'), value: `${successRate}% (${successCount}/${successCount + failCount})`, inline: true, }, { name: t(locale, 'commands.fishing.totalGoldEarned'), value: `${totalGoldEarned} G`, inline: true, }, { name: t(locale, 'commands.fishing.bestCatchReward'), value: `${bestCatchReward} G`, inline: true, }, { name: t(locale, 'commands.fishing.lastCastAt'), value: profile?.lastCastAt ? `` : t(locale, 'commands.fishing.noRecord'), inline: true, }, { name: t(locale, 'commands.fishing.rarityBreakdown'), value: rarityBreakdown, inline: false, }, ); if (!profile) { embed.setDescription(t(locale, 'commands.fishing.profileEmpty')); } await interaction.editReply({ embeds: [embed] }); return; } if (subcommand === 'dex') { const targetUser = interaction.options.getUser('user') ?? interaction.user; const collection = await FishingService.getCollection(targetUser.id, interaction.guildId); const embed = new EmbedBuilder() .setColor(0x14b8a6) .setTitle(t(locale, 'commands.fishing.dexTitle', { user: targetUser.username })) .setThumbnail(targetUser.displayAvatarURL()); if (!collection.length) { embed.setDescription(t(locale, 'commands.fishing.dexEmpty')); await interaction.editReply({ embeds: [embed] }); return; } for (const entry of collection.slice(0, 10)) { const fishName = FishingService.getFishDisplayName(entry.fishId); const rarityName = FishingService.getRarityDisplayNameById(entry.bestRarityId, locale); embed.addFields({ name: fishName, value: [ `${t(locale, 'commands.fishing.catchCount')}: ${entry.catchCount}`, `${t(locale, 'commands.fishing.bestRarity')}: ${rarityName}`, `${t(locale, 'commands.fishing.bestSize')}: ${entry.bestSizeCm.toFixed(1)} cm`, ].join('\n'), inline: true, }); } await interaction.editReply({ embeds: [embed] }); return; } if (subcommand === 'ranking') { const ranking = await FishingService.getSizeRanking(interaction.guildId); const embed = new EmbedBuilder() .setColor(0xf59e0b) .setTitle(t(locale, 'commands.fishing.rankingTitle')); if (!ranking.length) { embed.setDescription(t(locale, 'commands.fishing.rankingEmpty')); await interaction.editReply({ embeds: [embed] }); return; } for (const [index, entry] of ranking.entries()) { const fishName = FishingService.getFishDisplayName(entry.fishId); const rarityName = FishingService.getRarityDisplayNameById(entry.bestRarityId, locale); embed.addFields({ name: `#${index + 1} <@${entry.userId}>`, value: [ `${t(locale, 'commands.fishing.targetFish')}: ${fishName}`, `${t(locale, 'commands.fishing.rarity')}: ${rarityName}`, `${t(locale, 'commands.fishing.size')}: ${entry.bestSizeCm.toFixed(1)} cm`, ].join('\n'), inline: false, }); } await interaction.editReply({ embeds: [embed] }); } }, };