Kord/src/commands/fishing.ts

298 lines
10 KiB
TypeScript

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') {
await interaction.deferReply({ ephemeral: true });
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') {
await interaction.deferReply({ ephemeral: true });
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') {
await interaction.deferReply({ ephemeral: true });
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:${Math.floor(profile.lastCastAt.getTime() / 1000)}:R>`
: 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.reply({ embeds: [embed], ephemeral: true });
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.reply({ embeds: [embed], ephemeral: true });
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.reply({ embeds: [embed], ephemeral: true });
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.reply({ embeds: [embed], ephemeral: true });
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.reply({ embeds: [embed], ephemeral: true });
}
},
};