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)
}