298 lines
10 KiB
TypeScript
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 });
|
|
}
|
|
},
|
|
};
|