diff --git a/discord-impl-jda/src/main/kotlin/tw/waterballsa/utopia/jda/extensions/GenericCommandInteractionEventExtension.kt b/discord-impl-jda/src/main/kotlin/tw/waterballsa/utopia/jda/extensions/GenericCommandInteractionEventExtension.kt index eaf33b55..28e8edc1 100644 --- a/discord-impl-jda/src/main/kotlin/tw/waterballsa/utopia/jda/extensions/GenericCommandInteractionEventExtension.kt +++ b/discord-impl-jda/src/main/kotlin/tw/waterballsa/utopia/jda/extensions/GenericCommandInteractionEventExtension.kt @@ -2,8 +2,7 @@ package tw.waterballsa.utopia.jda.extensions import net.dv8tion.jda.api.events.interaction.command.GenericCommandInteractionEvent import net.dv8tion.jda.api.interactions.callbacks.IReplyCallback -import net.dv8tion.jda.api.interactions.commands.Command -import net.dv8tion.jda.api.interactions.commands.Command.* +import net.dv8tion.jda.api.interactions.commands.Command.Choice import net.dv8tion.jda.api.interactions.commands.OptionMapping import net.dv8tion.jda.api.interactions.commands.OptionType import net.dv8tion.jda.api.interactions.commands.build.OptionData @@ -75,16 +74,22 @@ fun GenericCommandInteractionEvent.getOptionWithValidation(name: String, return null } +fun SlashCommandData.addOptionalOption(type: OptionType, name: String, description: String, vararg choices: Choice) = + addOption(type, name, description, false, *choices) + fun SlashCommandData.addRequiredOption(type: OptionType, name: String, description: String, vararg choices: Choice) = - addOptions( - OptionData(type, name, description, true) - .addChoices(*choices) - ) + addOption(type, name, description, true, *choices) + +fun SlashCommandData.addOption(type: OptionType, name: String, description: String, isRequired: Boolean, vararg choices: Choice) = + addOptions(OptionData(type, name, description, isRequired).addChoices(*choices)) + +fun SubcommandData.addOptionalOption(type: OptionType, name: String, description: String, vararg choices: Choice) = + addOption(type, name, description, false, *choices) fun SubcommandData.addRequiredOption(type: OptionType, name: String, description: String, vararg choices: Choice) = - addOptions( - OptionData(type, name, description, true) - .addChoices(*choices) - ) + addOptions(OptionData(type, name, description, true).addChoices(*choices)) + +fun SubcommandData.addOption(type: OptionType, name: String, description: String, isRequired: Boolean, vararg choices: Choice) = + addOptions(OptionData(type, name, description, isRequired).addChoices(*choices)) fun IReplyCallback.replyEphemerally(message: String) = reply(message).setEphemeral(true).queue() diff --git a/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/RegisterGamificationCommand.kt b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/RegisterGamificationCommand.kt new file mode 100644 index 00000000..5e393f6c --- /dev/null +++ b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/RegisterGamificationCommand.kt @@ -0,0 +1,44 @@ +package tw.waterballsa.utopia.utopiagamification + +import net.dv8tion.jda.api.entities.Guild +import net.dv8tion.jda.api.interactions.commands.Command +import net.dv8tion.jda.api.interactions.commands.OptionType.STRING +import net.dv8tion.jda.api.interactions.commands.build.CommandData +import net.dv8tion.jda.api.interactions.commands.build.Commands +import net.dv8tion.jda.api.interactions.commands.build.SubcommandData +import org.springframework.stereotype.Component +import tw.waterballsa.utopia.jda.extensions.addOptionalOption +import tw.waterballsa.utopia.utopiagamification.quest.listeners.UtopiaGamificationListener +import tw.waterballsa.utopia.utopiagamification.repositories.PlayerRepository + +const val UTOPIA_COMMAND_NAME = "utopia" +const val FIRST_QUEST_COMMAND_NAME = "first-quest" +const val REVIEW_COMMAND_NAME = "re-render" +const val OPTION_COMMAND_NAME = "options" + +private const val LEADERBOARD_COMMAND_NAME = "leaderboard" +private const val LEADERBOARD_OPTION_MY_RANK = "my-rank" + +@Component +class RegisterGamificationCommand( + guild : Guild, + playerRepository: PlayerRepository +) : UtopiaGamificationListener(guild, playerRepository){ + override fun commands(): List = listOf( + Commands.slash(UTOPIA_COMMAND_NAME, "utopia command") + .addSubcommands( + // 任務系統 + SubcommandData(FIRST_QUEST_COMMAND_NAME, "get first quest"), + // 查詢並重發任務 + SubcommandData(REVIEW_COMMAND_NAME, "re-render in_progress/completed quest"), + // 排行榜 + SubcommandData(LEADERBOARD_COMMAND_NAME, "leaderboard") + .addOptionalOption( + STRING, + OPTION_COMMAND_NAME, + LEADERBOARD_OPTION_MY_RANK, + Command.Choice(LEADERBOARD_OPTION_MY_RANK, LEADERBOARD_OPTION_MY_RANK) + ) + ) + ) +} diff --git a/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/leaderboard/LeaderBoardListener.kt b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/leaderboard/LeaderBoardListener.kt new file mode 100644 index 00000000..43f1f59b --- /dev/null +++ b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/leaderboard/LeaderBoardListener.kt @@ -0,0 +1,164 @@ +package tw.waterballsa.utopia.utopiagamification.leaderboard + +import dev.minn.jda.ktx.messages.Embed +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent +import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent +import net.dv8tion.jda.api.interactions.components.buttons.Button +import org.springframework.stereotype.Component +import tw.waterballsa.utopia.gamification.leaderboard.domain.LeaderBoardItem +import tw.waterballsa.utopia.jda.UtopiaListener +import tw.waterballsa.utopia.utopiagamification.leaderboard.repository.LeaderBoardRepository +import tw.waterballsa.utopia.utopiagamification.repositories.query.Page +import tw.waterballsa.utopia.utopiagamification.repositories.query.PageRequest +import tw.waterballsa.utopia.utopiagamification.repositories.query.Pageable + +private const val UTOPIA_COMMAND_NAME = "utopia" +private const val LEADERBOARD_PREVIOUS_BUTTON = "utopia-leaderboard-previous" +private const val LEADERBOARD_NEXT_BUTTON = "utopia-leaderboard-next" +private const val LEADERBOARD_OPTION = "options" +private const val LEADERBOARD_SUBCOMMAND_NAME = "leaderboard" +private const val LEADERBOARD_MY_RANK = "my-rank" +private const val LEADERBOARD_PREVIOUS_STRING = "上一頁" +private const val LEADERBOARD_NEXT_STRING = "下一頁" + +@Component +class LeaderBoardListener( + private val leaderBoardRepository: LeaderBoardRepository, +) : UtopiaListener() { + + override fun onSlashCommandInteraction(event: SlashCommandInteractionEvent) { + + with(event) { + if (name != UTOPIA_COMMAND_NAME || subcommandName != LEADERBOARD_SUBCOMMAND_NAME) { + return + } + + val isLeaderboardQuery = options.isEmpty() + if (isLeaderboardQuery) { + queryLeaderboard() + } + + val leaderboardOption = getOption(LEADERBOARD_OPTION)?.asString ?: return + + val isSelfRankQuery = leaderboardOption == LEADERBOARD_MY_RANK + if (isSelfRankQuery) { + querySelfRank() + } + } + } + + /** + * 使用 Discord Embedded Message: + * - 印出多列:` <@userId> Lv.<等級> Exp: <經驗值> $<賞金>` ,每一列代表一個排名。 + * Discord Embedded Message 下方有兩個按鈕,”Previous Page” 和 “Next Page”。 + */ + private fun SlashCommandInteractionEvent.queryLeaderboard() { + val pageable = PageRequest.of(0, 10) + val page = leaderBoardRepository.findAll(pageable) + + reply("").addEmbeds( + Embed { description = page.createLeaderBoardRankDescription() } + ).addActionRow( + createLeaderBoardEmbedButton(page) + ).queue() + } + + /** + * 假設是 leaderboard my-rank 指令的話,會去 query 指定的 player + * 然後 output「妳的排名為第 N 名」到 discord channel + */ + private fun SlashCommandInteractionEvent.querySelfRank() { + val rank = leaderBoardRepository.queryPlayerRank(user.id)?.let { + "你的排名為第 ${it.rank} 名" + } ?: "找不到你的排名" + reply("").addEmbeds( + Embed { description = rank } + ).queue() + } + + override fun onButtonInteraction(event: ButtonInteractionEvent) { + with(event) { + if (!button.isLeaderBoardButton()) { + return + } + + deferEdit().queue() + + val pageable: Pageable = button.toPageable() + val page = leaderBoardRepository.findAll(pageable) + + hook.editMessageEmbedsById( + messageId, + Embed { + description = page.createLeaderBoardRankDescription() + } + ).setActionRow( + createLeaderBoardEmbedButton(page) + ).queue() + } + } + + private fun Button.isLeaderBoardButton(): Boolean { + return listOf( + LEADERBOARD_PREVIOUS_BUTTON, + LEADERBOARD_NEXT_BUTTON + ).any { id.toString().contains(it) } + } + + private fun Button.toPageable(): Pageable { + val splitButtonIds = id.toString().split("_") + return when (splitButtonIds.size) { + 3 -> { + PageRequest.of(splitButtonIds[1].toInt(), splitButtonIds[2].toInt()) + } + else -> { + PageRequest.ofSize(10) + } + } + } + + private fun Page.createLeaderBoardRankDescription(): String { + return getContent().joinToString(separator = "\n") { + "[${it.rank}] ${it.name}, ${it.level}, EXP=${it.exp}, Bounty=${it.bounty}" + } + } + + private fun createLeaderBoardEmbedButton(page: Page) : List