diff --git a/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/model/Monster.kt b/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/model/Monster.kt index 4a4d1a78..50d57677 100644 --- a/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/model/Monster.kt +++ b/domain/monster/core/src/commonMain/kotlin/br/alexandregpereira/hunter/domain/model/Monster.kt @@ -21,6 +21,7 @@ import br.alexandregpereira.hunter.domain.monster.spell.model.SpellPreview import br.alexandregpereira.hunter.domain.monster.spell.model.SpellUsage import br.alexandregpereira.hunter.domain.monster.spell.model.Spellcasting import br.alexandregpereira.hunter.domain.monster.spell.model.SpellcastingType +import java.text.NumberFormat import kotlin.native.ObjCName @ObjCName(name = "Monster", exact = true) @@ -53,8 +54,11 @@ data class Monster( val reactions: List = emptyList(), val spellcastings: List = emptyList(), val lore: String? = null, - val isClone: Boolean = false -) + val isClone: Boolean = false, +) { + + val xp: Int = challengeRatingToXp() +} data class MonsterImageData( val url: String = "", @@ -69,6 +73,59 @@ data class Color( fun Monster.isComplete() = abilityScores.isNotEmpty() +private fun Monster.challengeRatingToXp(): Int { + return when (challengeRating) { + 0.125f -> 25 + 0.25f -> 50 + 0.5f -> 100 + 1f -> 200 + 2f -> 450 + 3f -> 700 + 4f -> 1100 + 5f -> 1800 + 6f -> 2300 + 7f -> 2900 + 8f -> 3900 + 9f -> 5000 + 10f -> 5900 + 11f -> 7200 + 12f -> 8400 + 13f -> 10000 + 14f -> 11500 + 15f -> 13000 + 16f -> 15000 + 17f -> 18000 + 18f -> 20000 + 19f -> 22000 + 20f -> 25000 + 21f -> 33000 + 22f -> 41000 + 23f -> 50000 + 24f -> 62000 + 25f -> 75000 + 26f -> 90000 + 27f -> 105000 + 28f -> 120000 + 29f -> 135000 + 30f -> 155000 + else -> 10 + } +} + +fun Monster.xpFormatted(): String { + val xpString = when { + xp < 1000 -> xp.toString() + else -> { + val xpFormatted = NumberFormat.getIntegerInstance().format(xp) + .dropLastWhile { it == '0' } + .let { if (it.last().isDigit().not()) it.dropLast(1) else it } + "${xpFormatted}k" + } + } + + return "$xpString XP" +} + fun getFakeMonster(): Monster { return Monster( index = "1", diff --git a/feature/monster-detail/android/src/main/kotlin/br/alexandregpereira/hunter/detail/MonsterDetailStateMapper.kt b/feature/monster-detail/android/src/main/kotlin/br/alexandregpereira/hunter/detail/MonsterDetailStateMapper.kt index f1cf4ac2..7dc38a3d 100644 --- a/feature/monster-detail/android/src/main/kotlin/br/alexandregpereira/hunter/detail/MonsterDetailStateMapper.kt +++ b/feature/monster-detail/android/src/main/kotlin/br/alexandregpereira/hunter/detail/MonsterDetailStateMapper.kt @@ -50,8 +50,10 @@ import br.alexandregpereira.hunter.domain.model.MonsterType import br.alexandregpereira.hunter.domain.model.SavingThrow import br.alexandregpereira.hunter.domain.model.Skill import br.alexandregpereira.hunter.domain.model.SpeedValue +import br.alexandregpereira.hunter.domain.model.xpFormatted import br.alexandregpereira.hunter.domain.monster.spell.model.Spellcasting import br.alexandregpereira.hunter.ui.compose.SchoolOfMagicState +import java.text.NumberFormat internal fun List.asState(): List { return this.map { it.asState() } @@ -61,7 +63,12 @@ private fun Monster.asState(): MonsterState { return MonsterState( index = index, name = name, - imageState = imageData.asState(type, challengeRating, contentDescription = name), + imageState = imageData.asState( + type = type, + challengeRating = challengeRating, + xp = xpFormatted(), + contentDescription = name + ), subtype = subtype, group = group, subtitle = subtitle, @@ -101,6 +108,7 @@ private fun Monster.asState(): MonsterState { private fun MonsterImageData.asState( type: MonsterType, challengeRating: Float, + xp: String, contentDescription: String, ): MonsterImageState { return MonsterImageState( @@ -111,6 +119,7 @@ private fun MonsterImageData.asState( dark = backgroundColor.dark, ), challengeRating = challengeRating, + xp = xp, isHorizontal = isHorizontal, contentDescription = contentDescription, ) diff --git a/feature/monster-detail/android/src/main/kotlin/br/alexandregpereira/hunter/detail/ui/MonsterDetailScreen.kt b/feature/monster-detail/android/src/main/kotlin/br/alexandregpereira/hunter/detail/ui/MonsterDetailScreen.kt index 46f5ca33..f78bd79a 100644 --- a/feature/monster-detail/android/src/main/kotlin/br/alexandregpereira/hunter/detail/ui/MonsterDetailScreen.kt +++ b/feature/monster-detail/android/src/main/kotlin/br/alexandregpereira/hunter/detail/ui/MonsterDetailScreen.kt @@ -365,8 +365,10 @@ private fun ChallengeRatingCompose( AlphaTransition(dataList = monsters, pagerState, modifier = modifier) { data: MonsterState -> ChallengeRatingCircle( challengeRating = data.imageState.challengeRating, - size = 56.dp, - fontSize = 16.sp, + xp = data.imageState.xp, + size = 62.dp, + fontSize = 18.sp, + xpFontSize = 12.sp, contentTopPadding = contentTopPadding ) } @@ -410,6 +412,7 @@ private fun MonsterDetailPreview() = Window { url = "", type = MonsterTypeState.CELESTIAL, challengeRating = 0.0f, + xp = "100 XP", backgroundColor = ColorState( light = "#ffe2e2", dark = "#ffe2e2" @@ -459,6 +462,7 @@ private fun MonsterTopBarPreview() = Window { url = "", type = MonsterTypeState.CELESTIAL, challengeRating = 0.0f, + xp = "100 XP", backgroundColor = ColorState( light = "#ffe2e2", dark = "#ffe2e2" diff --git a/feature/monster-detail/android/src/main/kotlin/br/alexandregpereira/hunter/detail/ui/State.kt b/feature/monster-detail/android/src/main/kotlin/br/alexandregpereira/hunter/detail/ui/State.kt index 4c804d2e..233a4b4a 100644 --- a/feature/monster-detail/android/src/main/kotlin/br/alexandregpereira/hunter/detail/ui/State.kt +++ b/feature/monster-detail/android/src/main/kotlin/br/alexandregpereira/hunter/detail/ui/State.kt @@ -32,6 +32,7 @@ data class MonsterState( dark = "" ), challengeRating = 0.0f, + xp = "", isHorizontal = false, contentDescription = "" ), @@ -175,6 +176,7 @@ data class MonsterImageState( val type: MonsterTypeState, val backgroundColor: ColorState, val challengeRating: Float, + val xp: String, val isHorizontal: Boolean = false, val contentDescription: String = "" ) diff --git a/ui/core/src/main/kotlin/br/alexandregpereira/hunter/ui/compose/ChallengeRatingCircle.kt b/ui/core/src/main/kotlin/br/alexandregpereira/hunter/ui/compose/ChallengeRatingCircle.kt index f1413f52..4a6d55b6 100644 --- a/ui/core/src/main/kotlin/br/alexandregpereira/hunter/ui/compose/ChallengeRatingCircle.kt +++ b/ui/core/src/main/kotlin/br/alexandregpereira/hunter/ui/compose/ChallengeRatingCircle.kt @@ -17,10 +17,16 @@ package br.alexandregpereira.hunter.ui.compose import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable @@ -42,12 +48,13 @@ fun ChallengeRatingCircle( challengeRating: Float, size: Dp, modifier: Modifier = Modifier, - fontSize: TextUnit = 14.sp, - contentTopPadding: Dp = 0.dp + xp: String = "", + fontSize: TextUnit = 16.sp, + contentTopPadding: Dp = 0.dp, + xpFontSize: TextUnit = 10.sp, ) = Box( - contentAlignment = Alignment.CenterStart, modifier = modifier - .width(size) + .widthIn(min = size) .height(size + contentTopPadding) ) { DrawChallengeRatingCircle( @@ -55,20 +62,45 @@ fun ChallengeRatingCircle( canvasSize = size, contentTopPadding = contentTopPadding ) - Text( - challengeRating.getChallengeRatingFormatted(), - fontWeight = FontWeight.SemiBold, - fontSize = fontSize, - color = MaterialTheme.colors.onSurface, - textAlign = TextAlign.Center, - maxLines = 1, - modifier = Modifier - .width(size - 16.dp) - .padding( - bottom = 16.dp, - top = contentTopPadding + 4.dp + Column( + modifier = Modifier.background( + color = MaterialTheme.colors.surface, + shape = RoundedCornerShape(bottomEndPercent = 25) + ) + ) { + Spacer(modifier = Modifier.height(4.dp)) + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .padding(vertical = 4.dp) + .padding(top = contentTopPadding) + ) { + Text( + challengeRating.getChallengeRatingFormatted(), + fontWeight = FontWeight.SemiBold, + fontSize = fontSize, + color = MaterialTheme.colors.onSurface, + textAlign = TextAlign.Center, + maxLines = 1, + modifier = Modifier + .width(size - 16.dp) ) - ) + + if (xp.isNotBlank()) { + Text( + xp, + fontWeight = FontWeight.Normal, + fontSize = xpFontSize, + color = MaterialTheme.colors.onSurface, + textAlign = TextAlign.Center, + maxLines = 1, + modifier = Modifier + .padding(start = 4.dp, end = 8.dp) + ) + } + } + } } @Composable @@ -110,13 +142,39 @@ private fun Float.getChallengeRatingFormatted(): String { } } +@Preview +@Composable +private fun ChallengeRatingWithXpPreview() { + HunterTheme { + ChallengeRatingCircle( + challengeRating = 10f, + xp = "111k XP", + size = 62.dp, + fontSize = 18.sp, + xpFontSize = 14.sp, + ) + } +} +@Preview +@Composable +private fun ChallengeRatingWithXpPreviewWithDifferentSize() { + HunterTheme { + ChallengeRatingCircle( + challengeRating = 10f, + xp = "155k XP", + size = 56.dp, + fontSize = 16.sp, + contentTopPadding = 24.dp + ) + } +} @Preview @Composable private fun ChallengeRatingPreview() { HunterTheme { - ChallengeRatingCircle(10f, 48.dp) + ChallengeRatingCircle(challengeRating = 10f, size = 48.dp) } } @@ -125,7 +183,7 @@ private fun ChallengeRatingPreview() { private fun ChallengeRatingPreviewWithDifferentSize() { HunterTheme { ChallengeRatingCircle( - 10f, + challengeRating = 10f, size = 56.dp, fontSize = 16.sp, contentTopPadding = 24.dp