diff --git a/utopia-gamification/pom.xml b/utopia-gamification/pom.xml index 07bbfdec..ea85851b 100644 --- a/utopia-gamification/pom.xml +++ b/utopia-gamification/pom.xml @@ -27,5 +27,10 @@ utopia-test-kit test + + tw.waterballsa.utopia + utopia-test-kit + ${revision} + diff --git a/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/HotFixTool.kt b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/HotFixTool.kt deleted file mode 100644 index cce1b1ca..00000000 --- a/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/HotFixTool.kt +++ /dev/null @@ -1,129 +0,0 @@ -package tw.waterballsa.utopia.utopiagamification - -import net.dv8tion.jda.api.entities.Guild -import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent -import net.dv8tion.jda.api.interactions.commands.OptionType -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 tw.waterballsa.utopia.utopiagamification.quest.domain.State -import tw.waterballsa.utopia.utopiagamification.quest.listeners.UtopiaGamificationListener -import tw.waterballsa.utopia.utopiagamification.repositories.MissionRepository -import tw.waterballsa.utopia.utopiagamification.repositories.PlayerRepository - -const val COMMAND_NAME = "hotfix" -const val FIND_COMMAND_NAME = "mission-log" -const val FIND_OPTION_NAME = "player" -const val CHECK_COMMAND_NAME = "check" - -//只能使用在 local 端。 -//@Component -class HotFixTool( - guild: Guild, - playerRepository: PlayerRepository, - private val missionRepository: MissionRepository -) : UtopiaGamificationListener(guild, playerRepository) { - - override fun commands(): List = listOf( - Commands.slash(COMMAND_NAME, "it is tools of fix quest system error") - .addSubcommands( - SubcommandData(FIND_COMMAND_NAME, "find repository state") - .addOption(OptionType.USER, FIND_OPTION_NAME, "quest player", true), - SubcommandData(CHECK_COMMAND_NAME, "check mission state fail"), - ) - ) - - override fun onSlashCommandInteraction(event: SlashCommandInteractionEvent) { - with(event) { - val commandInfo = fullCommandName.split(" ") - - if (commandInfo.first() != COMMAND_NAME) { - return - } - - when (commandInfo[1]) { - FIND_COMMAND_NAME -> handleFindCommand() - CHECK_COMMAND_NAME -> handleCheckCommand() - } - } - } - - private fun SlashCommandInteractionEvent.handleFindCommand() { - val user = getOption(FIND_OPTION_NAME)?.asUser ?: return - - deferReply().setEphemeral(true).queue() - - val missions = missionRepository.findAllByPlayerId(user.id) - var result = """ - |${user.effectiveName} (${user.id}) - |-------------------------------------------- - | - """.trimMargin() - - missions.ifEmpty { - result += "not found\n" - } - - missions.forEach { - result += "${it.quest.title}(${it.quest.id}) : state -> ${it.state}, date -> ${it.completedTime}\n" - } - - result += "--------------------------------------------\n" - - hook.editOriginal(result).queue() - } - - private fun SlashCommandInteractionEvent.handleCheckCommand() { - deferReply().setEphemeral(true).queue() - - val isOk = mutableListOf() - val notOK = mutableListOf() - val workerRound = mutableListOf() - val questProgressRate = mutableMapOf>() - - (10 downTo 1).forEach { - questProgressRate[it] = mutableListOf() - } - - missionRepository.findAllByQuestId(10).forEach { - isOk.add(it.player.id) - workerRound.add(it.player.id) - if (it.state == State.COMPLETED) { - questProgressRate.getOrDefault(10, mutableListOf()).add(it.player.id) - } - } - - (9 downTo 1).forEach { - val missions = missionRepository.findAllByQuestId(it) - - missions.forEach { mission -> - if (mission.state == State.COMPLETED) { - questProgressRate.getOrDefault(it, mutableListOf()).add(mission.player.id) - } - if (isOk.contains(mission.player.id).not() && notOK.contains(mission.player.id).not()) { - if (mission.state == State.IN_PROGRESS || mission.state == State.COMPLETED) { - isOk.add(mission.player.id) - } else { - notOK.add(mission.player.id) - } - } - } - } - - val rank = questProgressRate.map { - it.key to it.value.map { id -> - playerRepository.findPlayerById(id)?.name ?: id - } - } - - hook.editOriginal( - """ - not ok count: ${notOK.size} - is ok count: ${isOk.size} - -------------------------------------------------- - $rank - -------------------------------------------------- - """.trimIndent() - ).queue() - } -} diff --git a/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/quest/domain/Player.kt b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/quest/domain/Player.kt index 995b4322..c524fb47 100644 --- a/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/quest/domain/Player.kt +++ b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/quest/domain/Player.kt @@ -1,33 +1,39 @@ package tw.waterballsa.utopia.utopiagamification.quest.domain -import tw.waterballsa.utopia.utopiagamification.quest.extensions.LevelSheet -import tw.waterballsa.utopia.utopiagamification.quest.extensions.LevelSheet.Companion.calculateLevel +import tw.waterballsa.utopia.utopiagamification.quest.extensions.LevelSheet.Companion.toLevel import java.time.OffsetDateTime import java.time.OffsetDateTime.now -import kotlin.ULong.Companion.MIN_VALUE class Player( val id: String, var name: String, - var exp: ULong = MIN_VALUE, - var level: UInt = 1u, + exp: ULong = 0uL, + level: UInt = 1u, val joinDate: OffsetDateTime = now(), - var latestActivateDate: OffsetDateTime = now(), - var levelUpgradeDate: OffsetDateTime = now(), - // TODO achievement-system 這邊應該要改成 Role 陣列 - val jdaRoles: MutableList = mutableListOf(), + latestActivateDate: OffsetDateTime = now(), + levelUpgradeDate: OffsetDateTime? = null, + val jdaRoles: MutableList = mutableListOf() ) { - init { - calculateLevel() - } + var exp = exp + private set + + var level = level + private set + + var levelUpgradeDate = levelUpgradeDate + private set - val currentLevelExpLimit - get() = LevelSheet.getLevelRange(level.toInt()).expLimit + var latestActivateDate = latestActivateDate + private set fun gainExp(rewardExp: ULong) { exp += rewardExp - calculateLevel() + val newLevel = exp.toLevel() + if (newLevel != level) { + level = newLevel + levelUpgradeDate = now() + } activate() } @@ -39,14 +45,6 @@ class Player( jdaRoles.add(role) } - private fun calculateLevel() { - val newLevel = calculateLevel(exp) - if (newLevel > level) { - level = newLevel - levelUpgradeDate = now() - } - } - private fun activate() { latestActivateDate = now() } diff --git a/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/quest/extensions/LevelSheet.kt b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/quest/extensions/LevelSheet.kt index 6c6217b8..e5fbe463 100644 --- a/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/quest/extensions/LevelSheet.kt +++ b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/quest/extensions/LevelSheet.kt @@ -1,26 +1,25 @@ package tw.waterballsa.utopia.utopiagamification.quest.extensions -import tw.waterballsa.utopia.utopiagamification.quest.extensions.LevelSheet.Range.Companion.LEVEL_ONE -import kotlin.ULong.Companion.MIN_VALUE +import tw.waterballsa.utopia.utopiagamification.quest.extensions.LevelSheet.LevelRange.Companion.LEVEL_ONE +import java.lang.String.format + class LevelSheet private constructor() { companion object { - const val EXP_PER_MINUTES = 10u + private const val EXP_PER_MINUTES = 10u private const val MAX_LEVEL = 100 private val COEFFICIENTS = arrayOf(1u, 2u, 4u, 8u, 12u, 16u, 32u, 52u, 64u, 84u) - private val LEVEL_TO_RANGE = generateSequence(LEVEL_ONE) { it.next() }.take(MAX_LEVEL).associateBy { it.level } + private val LEVEL_TO_LEVEL_RANGE = generateSequence(LEVEL_ONE) { it.next() }.take(MAX_LEVEL).associateBy { it.level } - fun calculateLevel(exp: ULong) = (LEVEL_TO_RANGE.values.find { it.isExpGreaterThan(exp) } ?: LEVEL_ONE).level.toUInt() + // exp to level + fun ULong.toLevel() = (LEVEL_TO_LEVEL_RANGE.values.find { it.isMatchLevel(this) } ?: LEVEL_ONE).level.toUInt() - fun getLevelRange(level: Int): Range = when { - level <= 0 -> LEVEL_ONE - level > MAX_LEVEL -> LEVEL_TO_RANGE.values.last() - else -> LEVEL_TO_RANGE[level] ?: throw IllegalArgumentException("The level ($level) is incorrect.") - } + // level to level range + fun UInt.toLevelRange(): LevelRange = LEVEL_TO_LEVEL_RANGE[toInt()] ?: throw IllegalArgumentException("The given level ($this) not found.") } - class Range private constructor(val level: Int = 1, previousLevelRange: Range? = null) { + class LevelRange private constructor(val level: Int = 1, val previous: LevelRange? = null) { // 升級時間 val upgradeTime: ULong @@ -32,29 +31,22 @@ class LevelSheet private constructor() { val expLimit: ULong companion object { - val LEVEL_ONE = Range() + val LEVEL_ONE = LevelRange(level = 1) } init { // 經驗值係數 val coefficient = COEFFICIENTS[level.coerceAtLeast(1).div(10).coerceAtMost(COEFFICIENTS.size.minus(1))] - upgradeTime = (previousLevelRange?.upgradeTime ?: MIN_VALUE).plus(EXP_PER_MINUTES.times(coefficient)) - accExp = (previousLevelRange?.accExp ?: MIN_VALUE).plus(EXP_PER_MINUTES.times(upgradeTime)) - expLimit = accExp.minus(previousLevelRange?.accExp ?: MIN_VALUE) + upgradeTime = (previous?.upgradeTime ?: 0u).plus(EXP_PER_MINUTES.times(coefficient)) + accExp = (previous?.accExp ?: 0u).plus(EXP_PER_MINUTES.times(upgradeTime)) + expLimit = accExp.minus(previous?.accExp ?: 0u) } - fun next() = Range(level.plus(1), this) + fun next() = LevelRange(level.plus(1), this) - fun isExpGreaterThan(exp: ULong) = accExp > exp + fun isMatchLevel(exp: ULong) = accExp > exp - override fun toString(): String { - return String.format( - "level: %3d, upgrade time: %5d, exp limit: %6d, acc exp: %7d", - level, - upgradeTime.toLong(), - expLimit.toLong(), - accExp.toLong() - ) - } + override fun toString(): String = format("| level: %3d | upgrade time: %5d | exp limit: %6d | acc exp: %7d | %n${"-".repeat(75)}", + level, upgradeTime.toLong(), expLimit.toLong(), accExp.toLong()) } } diff --git a/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/quest/listeners/ButtonInteractionListener.kt b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/quest/listeners/ButtonInteractionListener.kt index 14bab171..0e21c8f7 100644 --- a/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/quest/listeners/ButtonInteractionListener.kt +++ b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/quest/listeners/ButtonInteractionListener.kt @@ -6,6 +6,9 @@ import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent import net.dv8tion.jda.api.interactions.components.buttons.ButtonStyle import org.springframework.stereotype.Component import tw.waterballsa.utopia.utopiagamification.quest.domain.Mission +import tw.waterballsa.utopia.utopiagamification.quest.domain.Player +import tw.waterballsa.utopia.utopiagamification.quest.extensions.LevelSheet.Companion.toLevelRange +import tw.waterballsa.utopia.utopiagamification.quest.extensions.LevelSheet.LevelRange.Companion.LEVEL_ONE import tw.waterballsa.utopia.utopiagamification.quest.extensions.publishToUser import tw.waterballsa.utopia.utopiagamification.quest.usecase.ClaimMissionRewardUsecase import tw.waterballsa.utopia.utopiagamification.repositories.PlayerRepository @@ -45,11 +48,12 @@ class UtopiaGamificationQuestListener( val request = ClaimMissionRewardUsecase.Request(player, questId) val presenter = object : ClaimMissionRewardUsecase.Presenter { override fun presentPlayerExpNotification(mission: Mission) { + publishMessage( """ - ${player.name} 已獲得 ${mission.quest.reward.exp} exp!! - 目前等級:${player.level} - 目前經驗值:${player.exp} / ${player.currentLevelExpLimit} + ${mission.player.name} 已獲得 ${mission.quest.reward.exp} exp!! + 目前等級:${mission.player.level} + 目前經驗值:${mission.player.currentExp()}/${mission.player.level.toLevelRange().expLimit} """.trimIndent() ) } @@ -67,6 +71,9 @@ class UtopiaGamificationQuestListener( } } + private fun Player.currentExp(): ULong = + if (level == LEVEL_ONE.level.toUInt()) exp else exp - level.toLevelRange().previous!!.accExp + private fun ButtonInteractionEvent.splitButtonId(delimiters: String): List { val result = button.id?.split(delimiters) ?: return emptyList() if (result.size != 3) { diff --git a/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/repositories/PlayerRepository.kt b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/repositories/PlayerRepository.kt index ed757864..03fef515 100644 --- a/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/repositories/PlayerRepository.kt +++ b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/repositories/PlayerRepository.kt @@ -6,4 +6,5 @@ interface PlayerRepository { fun findPlayerById(id: String): Player? fun savePlayer(player: Player): Player + } diff --git a/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/repositories/mongodb/repositoryimpl/MongodbPlayerRepository.kt b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/repositories/mongodb/repositoryimpl/MongodbPlayerRepository.kt index 51646841..d4acbc15 100644 --- a/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/repositories/mongodb/repositoryimpl/MongodbPlayerRepository.kt +++ b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/repositories/mongodb/repositoryimpl/MongodbPlayerRepository.kt @@ -48,6 +48,6 @@ data class PlayerDocument( val level: Int, val joinDate: OffsetDateTime, val latestActivateDate: OffsetDateTime, - val levelUpgradeDate: OffsetDateTime, + val levelUpgradeDate: OffsetDateTime?, val jdaRoles: MutableList? = mutableListOf() ) diff --git a/utopia-gamification/src/test/kotlin/tw/waterballsa/utopia/utopiagmification/quest/UtopiaGamificationQuestTest.kt b/utopia-gamification/src/test/kotlin/tw/waterballsa/utopia/utopiagmification/quest/UtopiaGamificationQuestTest.kt index 83828834..f46e1d3a 100644 --- a/utopia-gamification/src/test/kotlin/tw/waterballsa/utopia/utopiagmification/quest/UtopiaGamificationQuestTest.kt +++ b/utopia-gamification/src/test/kotlin/tw/waterballsa/utopia/utopiagmification/quest/UtopiaGamificationQuestTest.kt @@ -1,5 +1,6 @@ package tw.waterballsa.utopia.utopiagmification.quest +import org.assertj.core.api.Assertions.* import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -74,5 +75,15 @@ class UtopiaGamificationQuestTest { assertFalse(activityMission.isCompleted()) } + @Test + fun `given playerA level 3 and exp 650, when player gain 6000 exp, then level is 11 and exp is 6650`() { + val player = Player("A", "A", 650u, 3u) + val rewardExp = 6000uL + player.gainExp(rewardExp) + + assertThat(player.exp).isEqualTo(6650uL) + assertThat(player.level).isEqualTo(11u) + } + private fun Player.acceptQuest(quest: Quest) = Mission(this, quest) }