StatCalculatorServiceImpl.kt
package io.github.lishangbu.avalon.dataset.service.impl
import io.github.lishangbu.avalon.dataset.entity.dto.CreatureView
import io.github.lishangbu.avalon.dataset.entity.dto.NatureView
import io.github.lishangbu.avalon.dataset.model.StatCalculatorCreaturePresetView
import io.github.lishangbu.avalon.dataset.model.StatCalculatorCreatureStatPresetView
import io.github.lishangbu.avalon.dataset.model.StatCalculatorEntryRequest
import io.github.lishangbu.avalon.dataset.model.StatCalculatorEntryResultView
import io.github.lishangbu.avalon.dataset.model.StatCalculatorNatureView
import io.github.lishangbu.avalon.dataset.model.StatCalculatorRequest
import io.github.lishangbu.avalon.dataset.model.StatCalculatorResultView
import io.github.lishangbu.avalon.dataset.repository.CreatureRepository
import io.github.lishangbu.avalon.dataset.repository.CreatureStatRepository
import io.github.lishangbu.avalon.dataset.repository.NatureRepository
import io.github.lishangbu.avalon.dataset.repository.StatRepository
import io.github.lishangbu.avalon.dataset.service.StatCalculatorService
import io.github.lishangbu.avalon.game.calculator.stat.StatCalculatorFactory
import org.springframework.stereotype.Service
@Service
class StatCalculatorServiceImpl(
private val creatureRepository: CreatureRepository,
private val creatureStatRepository: CreatureStatRepository,
private val statRepository: StatRepository,
private val natureRepository: NatureRepository,
private val statCalculatorFactory: StatCalculatorFactory,
) : StatCalculatorService {
override fun getCreaturePreset(
creatureId: String?,
): StatCalculatorCreaturePresetView {
val creature = resolveCreature(creatureId)
val rows = creatureStatRepository.findAll().filter { row -> row.id.creatureId == creature.id.toLong() }
val rowsByStatId = rows.associateBy { row -> row.id.statId }
val coreStats = statRepository.listCoreStats()
val presetStats =
coreStats
.mapNotNull { stat ->
if (stat.battleOnly != false) {
return@mapNotNull null
}
val row = rowsByStatId[stat.id] ?: return@mapNotNull null
val internalName = stat.internalName ?: return@mapNotNull null
StatCalculatorCreatureStatPresetView(
statId = stat.id.toString(),
statInternalName = internalName,
statName = stat.name ?: internalName,
baseStat = row.baseStat ?: 0,
effortYield = 0,
)
}
val creatureInternalNameValue = creature.internalName ?: error("Creature internalName must not be null.")
return StatCalculatorCreaturePresetView(
creatureId = creature.id,
creatureInternalName = creatureInternalNameValue,
creatureName = creature.name ?: creatureInternalNameValue,
creatureSpeciesId = creature.creatureSpecies?.id,
creatureSpeciesInternalName = creature.creatureSpecies?.internalName,
creatureSpeciesName = creature.creatureSpecies?.name,
stats = presetStats,
)
}
override fun calculate(request: StatCalculatorRequest): StatCalculatorResultView {
require(request.level in 1..100) { "level must be between 1 and 100." }
require(request.stats.isNotEmpty()) { "stats must not be empty." }
val nature = resolveNature(request.natureId)
val statsById =
statRepository
.findAll()
.associateBy { stat -> stat.id }
val duplicatedStat =
request.stats
.groupBy { entry -> entry.statId }
.entries
.firstOrNull { entry -> entry.value.size > 1 }
?.key
require(duplicatedStat == null) { "Duplicate statId '$duplicatedStat' is not allowed." }
val results =
request.stats.map { entry ->
validateEntry(entry)
val stat = statsById[entry.statId] ?: error("Stat '${entry.statId}' was not found in dataset.")
require(stat.battleOnly == false) {
"Unsupported statId '${entry.statId}'."
}
val statInternalName =
stat.internalName ?: error("Stat '${entry.statId}' internalName must not be null.")
val natureModifier = resolveNatureModifier(statInternalName, nature)
val actualValue =
statCalculatorFactory.calculateStat(
statInternalName,
entry.baseStat,
entry.iv,
entry.ev,
request.level,
natureModifier,
)
val minimumValue =
statCalculatorFactory.calculateStat(
statInternalName,
entry.baseStat,
0,
entry.ev,
request.level,
natureModifier,
)
val maximumValue =
statCalculatorFactory.calculateStat(
statInternalName,
entry.baseStat,
31,
entry.ev,
request.level,
natureModifier,
)
StatCalculatorEntryResultView(
statId = stat.id.toString(),
statInternalName = statInternalName,
statName = stat.name ?: statInternalName,
baseStat = entry.baseStat,
iv = entry.iv,
ev = entry.ev,
actualValue = actualValue,
minimumValue = minimumValue,
maximumValue = maximumValue,
natureModifier = natureModifier,
)
}
return StatCalculatorResultView(
level = request.level,
totalEv = request.stats.sumOf { entry -> entry.ev },
nature = nature?.toCalculatorView(),
stats = results,
)
}
private fun resolveCreature(
creatureId: String?,
): CreatureView {
require(!creatureId.isNullOrBlank()) {
"creatureId must not be blank."
}
val parsedCreatureId = creatureId.toLongOrNull() ?: error("creatureId must be a valid long value.")
return requireNotNull(creatureRepository.loadViewById(parsedCreatureId)) {
"Creature '$creatureId' was not found."
}
}
private fun resolveNature(
natureId: Long?,
): NatureView? {
if (natureId == null) {
return null
}
return requireNotNull(natureRepository.loadViewById(natureId)) {
"Nature '$natureId' was not found."
}
}
private fun validateEntry(entry: StatCalculatorEntryRequest) {
require(entry.statId >= 1) { "statId must be greater than or equal to 1." }
require(entry.baseStat >= 1) { "baseStat must be greater than or equal to 1." }
require(entry.iv in 0..31) { "iv must be between 0 and 31." }
require(entry.ev in 0..252) { "ev must be between 0 and 252." }
}
private fun resolveNatureModifier(
statInternalName: String,
nature: NatureView?,
): Int {
if (nature == null || statInternalName == "hp") {
return 100
}
val increasedStat = nature.increasedStat?.internalName
val decreasedStat = nature.decreasedStat?.internalName
return when {
increasedStat == statInternalName && decreasedStat != statInternalName -> 110
decreasedStat == statInternalName && increasedStat != statInternalName -> 90
else -> 100
}
}
private fun NatureView.toCalculatorView(): StatCalculatorNatureView =
(internalName ?: error("Nature internalName must not be null.")).let { internalNameValue ->
StatCalculatorNatureView(
id = id,
internalName = internalNameValue,
name = name ?: internalNameValue,
increasedStatId = increasedStat?.id,
increasedStatInternalName = increasedStat?.internalName,
increasedStatName = increasedStat?.name,
decreasedStatId = decreasedStat?.id,
decreasedStatInternalName = decreasedStat?.internalName,
decreasedStatName = decreasedStat?.name,
)
}
}