diff --git a/app/src/main/java/com/pixelized/rplexicon/model/CharacterSheet.kt b/app/src/main/java/com/pixelized/rplexicon/model/CharacterSheet.kt index 84fa41c..4dcff17 100644 --- a/app/src/main/java/com/pixelized/rplexicon/model/CharacterSheet.kt +++ b/app/src/main/java/com/pixelized/rplexicon/model/CharacterSheet.kt @@ -17,6 +17,7 @@ data class CharacterSheet( val spell7: Counter?, val spell8: Counter?, val spell9: Counter?, + val criticalModifier: Int, // Critical Dice Multiplier val armorClass: String, // Classe d'armure val speed: Int, // Vitesse val strength: Int, // Force diff --git a/app/src/main/java/com/pixelized/rplexicon/model/Roll.kt b/app/src/main/java/com/pixelized/rplexicon/model/Roll.kt index 45f1675..1535019 100644 --- a/app/src/main/java/com/pixelized/rplexicon/model/Roll.kt +++ b/app/src/main/java/com/pixelized/rplexicon/model/Roll.kt @@ -1,6 +1,8 @@ package com.pixelized.rplexicon.model data class Roll( + val character: String, + val canUseCriticalDice: Boolean, val title: String, val highlight: String? = null, val dices: List, diff --git a/app/src/main/java/com/pixelized/rplexicon/repository/parser/CharacterSheetParser.kt b/app/src/main/java/com/pixelized/rplexicon/repository/parser/CharacterSheetParser.kt index 29681c5..26c364f 100644 --- a/app/src/main/java/com/pixelized/rplexicon/repository/parser/CharacterSheetParser.kt +++ b/app/src/main/java/com/pixelized/rplexicon/repository/parser/CharacterSheetParser.kt @@ -1,8 +1,8 @@ package com.pixelized.rplexicon.repository.parser import com.google.api.services.sheets.v4.model.ValueRange -import com.pixelized.rplexicon.repository.parser.alteration.CounterParser import com.pixelized.rplexicon.model.CharacterSheet +import com.pixelized.rplexicon.repository.parser.alteration.CounterParser import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure import com.pixelized.rplexicon.utilitary.extentions.local.checkSheetStructure import com.pixelized.rplexicon.utilitary.extentions.sheet @@ -54,50 +54,33 @@ class CharacterSheetParser @Inject constructor( maxHitPoint = item.parseString(MAX_HIT_POINT) ?: "1", lifeDice = item.parseInt(LIFE_DICE) ?: 0, spell1 = counterParser.parseCounter( - value = item.parseString( - SPELL_LEVEL_1 - ) + value = item.parseString(SPELL_LEVEL_1) ), spell2 = counterParser.parseCounter( - value = item.parseString( - SPELL_LEVEL_2 - ) + value = item.parseString(SPELL_LEVEL_2) ), spell3 = counterParser.parseCounter( - value = item.parseString( - SPELL_LEVEL_3 - ) + value = item.parseString(SPELL_LEVEL_3) ), spell4 = counterParser.parseCounter( - value = item.parseString( - SPELL_LEVEL_4 - ) + value = item.parseString(SPELL_LEVEL_4) ), spell5 = counterParser.parseCounter( - value = item.parseString( - SPELL_LEVEL_5 - ) + value = item.parseString(SPELL_LEVEL_5) ), spell6 = counterParser.parseCounter( - value = item.parseString( - SPELL_LEVEL_6 - ) + value = item.parseString(SPELL_LEVEL_6) ), spell7 = counterParser.parseCounter( - value = item.parseString( - SPELL_LEVEL_7 - ) + value = item.parseString(SPELL_LEVEL_7) ), spell8 = counterParser.parseCounter( - value = item.parseString( - SPELL_LEVEL_8 - ) + value = item.parseString(SPELL_LEVEL_8) ), spell9 = counterParser.parseCounter( - value = item.parseString( - SPELL_LEVEL_9 - ) + value = item.parseString(SPELL_LEVEL_9) ), + criticalModifier = item.parseInt(CRITICAL_MODIFIER) ?: 2, armorClass = item.parseString(ARMOR_CLASS) ?: "10", speed = item.parseInt(SPEED) ?: 10, strength = item.parseInt(STRENGTH) ?: 10, @@ -157,6 +140,7 @@ class CharacterSheetParser @Inject constructor( private const val SPELL_LEVEL_7 = "Sort de niveau 7" private const val SPELL_LEVEL_8 = "Sort de niveau 8" private const val SPELL_LEVEL_9 = "Sort de niveau 9" + private const val CRITICAL_MODIFIER = "Dé de critique" private const val ARMOR_CLASS = "Classe d'armure" private const val SPEED = "Vitesse" private const val MASTERY = "Bonus de maîtrise" @@ -208,6 +192,7 @@ class CharacterSheetParser @Inject constructor( SPELL_LEVEL_7, SPELL_LEVEL_8, SPELL_LEVEL_9, + CRITICAL_MODIFIER, ARMOR_CLASS, SPEED, MASTERY, diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/CharacterSheetScreen.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/CharacterSheetScreen.kt index 46c27b9..a31e629 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/CharacterSheetScreen.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/CharacterSheetScreen.kt @@ -212,7 +212,6 @@ private fun CharacterSheetContent( Surface( modifier = Modifier.padding(paddingValues = paddingValues), ) { - ModalBottomSheetLayout( sheetState = sheetState, sheetContent = { diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/actions/AttackActionViewModel.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/actions/AttackActionViewModel.kt index 43dff94..b44af8a 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/actions/AttackActionViewModel.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/actions/AttackActionViewModel.kt @@ -66,6 +66,7 @@ class AttackActionViewModel @Inject constructor( return actionRoll( attack = action, throws = action?.hit, + canUseCriticalDice = false, ) } @@ -74,12 +75,14 @@ class AttackActionViewModel @Inject constructor( return actionRoll( attack = action, throws = action?.damage, + canUseCriticalDice = true, ) } private fun actionRoll( attack: Attack?, throws: Throw?, + canUseCriticalDice: Boolean, ): Roll { // build the title val title = context.getString( @@ -123,6 +126,8 @@ class AttackActionViewModel @Inject constructor( val fail = alterations.any { it.fail } // build the roll return Roll( + character = character, + canUseCriticalDice = canUseCriticalDice, title = title, highlight = attack?.title, dices = listOf( diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/actions/SpellsActionViewModel.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/actions/SpellsActionViewModel.kt index c1fc593..1591f9d 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/actions/SpellsActionViewModel.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/actions/SpellsActionViewModel.kt @@ -209,6 +209,7 @@ class SpellsActionViewModel @Inject constructor( spell = spell?.spell, titleRes = R.string.dice_roll_attack_damage_title, throws = spell?.effect, + canUseCriticalDice = true, ) } @@ -221,11 +222,14 @@ class SpellsActionViewModel @Inject constructor( throws: Throw?, level: Int = 0, levelThrow: Throw? = null, + canUseCriticalDice: Boolean = false, ): Roll { // build the title val title = context.getString(titleRes, spell?.name) // build the roll return Roll( + character = character, + canUseCriticalDice = canUseCriticalDice, title = title, highlight = spell?.name, dices = listOf( diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/proficiency/ProficiencyViewModel.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/proficiency/ProficiencyViewModel.kt index bc0546a..5e278b8 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/proficiency/ProficiencyViewModel.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/proficiency/ProficiencyViewModel.kt @@ -298,6 +298,8 @@ class ProficiencyViewModel @Inject constructor( val fail = alterations.any { it.fail } return Roll( + character = character, + canUseCriticalDice = false, title = context.getString(R.string.dice_roll_check_title, ability.uppercase()), highlight = ability, dices = listOf( @@ -337,6 +339,8 @@ class ProficiencyViewModel @Inject constructor( val fail = alterations.any { it.fail } return Roll( + character = character, + canUseCriticalDice = false, title = context.getString( R.string.dice_roll_saving_throw_title, ability.uppercase() @@ -388,6 +392,8 @@ class ProficiencyViewModel @Inject constructor( val fail = alterations.any { it.fail } return Roll( + character = character, + canUseCriticalDice = false, title = context.getString(R.string.dice_roll_check_title, ability.uppercase()), highlight = ability, dices = listOf( diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/ConvertRollIntoDisplayableFactory.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/ConvertRollIntoDisplayableFactory.kt index 6e06433..7fc360e 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/ConvertRollIntoDisplayableFactory.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/ConvertRollIntoDisplayableFactory.kt @@ -9,16 +9,23 @@ import javax.inject.Inject import kotlin.math.max import kotlin.math.min +data class RollResult( + val dice: RollDiceUio?, + val card: ThrowsCardUio?, +) + class ConvertRollIntoDisplayableFactory @Inject constructor() { - fun roll(roll: Roll): Pair { - + fun roll( + roll: Roll, + diceMultiplier: Int = 1, + ): RollResult { val mainDices = roll.dices.firstOrNull() val bonusDicesList = roll.dices.subList(fromIndex = 1, toIndex = roll.dices.size) val allRolledValues = mutableListOf() val (rollDice, rollDetail) = mainDices?.let { dice -> - val (label, sum) = dice.roll() + val (label, sum) = dice.roll(multiplier = diceMultiplier) allRolledValues.add(sum) val diceIcon = dice.toRollCardUio(result = sum) @@ -31,7 +38,7 @@ class ConvertRollIntoDisplayableFactory @Inject constructor() { val isCriticalFailure = rollDice?.isCriticalFailure ?: false val diceBonus = bonusDicesList.map { dice -> - val (label, sum) = dice.roll() + val (label, sum) = dice.roll(multiplier = diceMultiplier) allRolledValues.add(sum) dice.toThrowsCardUio( @@ -49,23 +56,25 @@ class ConvertRollIntoDisplayableFactory @Inject constructor() { ) } - return rollDice to ThrowsCardUio( - title = roll.title, - highlight = roll.highlight, - dice = mainDices?.faces?.icon, - roll = allRolledValues.toLabel(), - result = when { - isCriticalSuccess -> (mainDices?.faces ?: 20).toString() - isCriticalFailure -> "1" - else -> "${allRolledValues.sum()}" - }, - isCriticalSuccess = isCriticalSuccess, - isCriticalFailure = isCriticalFailure, - details = rollDetail + diceBonus + flatBonus, + return RollResult( + dice = rollDice, + card = ThrowsCardUio( + title = roll.title, + highlight = roll.highlight, + dice = mainDices?.faces?.icon, + roll = allRolledValues.toLabel(), + result = when { + isCriticalSuccess -> (mainDices?.faces ?: 20).toString() + isCriticalFailure -> "1" + else -> "${allRolledValues.sum()}" + }, + isCriticalSuccess = isCriticalSuccess, + isCriticalFailure = isCriticalFailure, + details = rollDetail + diceBonus + flatBonus, + ), ) } - private fun Roll.Dice.toRollCardUio(result: Int) = RollDiceUio( icon = faces.icon, isCriticalSuccess = count == 1 && faces == 20 && result == faces, @@ -85,33 +94,33 @@ class ConvertRollIntoDisplayableFactory @Inject constructor() { result = "$result", ) - private data class RollResult( + private data class DiceRollResult( val label: String, val sum: Int, ) - private fun Roll.Dice.roll(): RollResult { + private fun Roll.Dice.roll(multiplier: Int): DiceRollResult { return when { advantage && !disadvantage -> { - val roll = List(count) { random() to random() } - RollResult( + val roll = List(count * multiplier) { random() to random() } + DiceRollResult( label = roll.joinToString(" + ") { "${it.first}~${it.second}" }, sum = roll.sumOf { max(it.first, it.second) }, ) } disadvantage && !advantage -> { - val roll = List(count) { random() to random() } - RollResult( + val roll = List(count * multiplier) { random() to random() } + DiceRollResult( label = roll.joinToString(" + ") { "${it.first}~${it.second}" }, sum = roll.sumOf { min(it.first, it.second) }, ) } else -> { - val roll = List(count) { random() } - RollResult( + val roll = List(count * multiplier) { random() } + DiceRollResult( label = roll.toLabel(), sum = roll.sum(), ) diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/RollOverlay.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/RollOverlay.kt index 18a58bf..96b3a3c 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/RollOverlay.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/RollOverlay.kt @@ -2,6 +2,7 @@ package com.pixelized.rplexicon.ui.screens.rolls import android.content.res.Configuration.UI_MODE_NIGHT_NO import android.content.res.Configuration.UI_MODE_NIGHT_YES +import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedContent import androidx.compose.animation.SizeTransform import androidx.compose.animation.fadeIn @@ -11,14 +12,26 @@ import androidx.compose.animation.slideOutVertically import androidx.compose.animation.togetherWith import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Close +import androidx.compose.material.icons.outlined.Menu +import androidx.compose.material3.DrawerState +import androidx.compose.material3.DrawerValue +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.ModalNavigationDrawer +import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberDrawerState import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.Stable @@ -26,124 +39,194 @@ import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.pixelized.rplexicon.LocalRollOverlay +import com.pixelized.rplexicon.NO_WINDOW_INSETS import com.pixelized.rplexicon.R import com.pixelized.rplexicon.model.Roll import com.pixelized.rplexicon.ui.composable.BlurredOverlayHostState +import com.pixelized.rplexicon.ui.screens.rolls.composable.RollAlteration +import com.pixelized.rplexicon.ui.screens.rolls.composable.RollAlterationUio import com.pixelized.rplexicon.ui.screens.rolls.composable.RollDice import com.pixelized.rplexicon.ui.screens.rolls.composable.RollDiceUio import com.pixelized.rplexicon.ui.screens.rolls.composable.ThrowsCard import com.pixelized.rplexicon.ui.screens.rolls.composable.ThrowsCardUio import com.pixelized.rplexicon.ui.theme.LexiconTheme +import kotlinx.coroutines.launch @Composable fun RollOverlay( viewModel: RollOverlayViewModel = hiltViewModel(), ) { val overlay = LocalRollOverlay.current + val scope = rememberCoroutineScope() + val drawer = rememberDrawerState(initialValue = DrawerValue.Closed) RollOverlayContent( modifier = Modifier.fillMaxSize(), + drawer = drawer, dice = viewModel.dice, card = viewModel.card, + alterations = viewModel.alterations, showDetail = viewModel.showDetail, + onMenu = { + scope.launch { drawer.open() } + }, + onClose = { + overlay.hideOverlay() + }, onDice = { viewModel.roll() }, onCard = { viewModel.toggleDetail() }, - onClose = { - overlay.hideOverlay() - } + onAlteration = { + viewModel.onAlteration(it) + }, ) + + BackHandler(enabled = drawer.isOpen) { + scope.launch { drawer.close() } + } } +@OptIn(ExperimentalMaterial3Api::class) @Composable private fun RollOverlayContent( modifier: Modifier = Modifier, + drawer: DrawerState, dice: State, card: State, + alterations: State>, showDetail: State, + onMenu: () -> Unit, + onClose: () -> Unit, onDice: () -> Unit, onCard: () -> Unit, - onClose: () -> Unit, + onAlteration: (id: String) -> Unit, ) { val density = LocalDensity.current - Box( + ModalNavigationDrawer( modifier = modifier, - ) { - RollDice( - modifier = Modifier - .align(alignment = Alignment.Center) - .padding(horizontal = 16.dp), - dice = dice, - onDice = onDice, - ) - - AnimatedContent( - modifier = Modifier - .fillMaxWidth() - .align(Alignment.BottomCenter) - .padding(all = 16.dp), - targetState = card.value, - transitionSpec = { - val enter = fadeIn() + slideInVertically { with(density) { 64.dp.roundToPx() } } - val exit = fadeOut() + slideOutVertically { with(density) { -64.dp.roundToPx() } } - val transform = SizeTransform(clip = false) - enter togetherWith exit using transform - }, - label = "RollOverlayDisplay", - ) { - when (it) { - null -> Box( - modifier = Modifier.fillMaxWidth(), - ) - - else -> ThrowsCard( + drawerState = drawer, + drawerContent = { + Surface { + LazyColumn( modifier = Modifier - .fillMaxWidth() - .clickable(onClick = onCard), - throws = it, - showDetail = showDetail, - ) + .fillMaxHeight() + .fillMaxWidth(4f / 5f), + ) { + items(items = alterations.value) { + RollAlteration( + modifier = Modifier.fillMaxWidth(), + alteration = it, + onClick = onAlteration, + ) + } + } } - } + }, + content = { + Scaffold( + containerColor = Color.Transparent, + contentWindowInsets = NO_WINDOW_INSETS, + topBar = { + TopAppBar( + colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), + title = { }, + navigationIcon = { + IconButton(onClick = onMenu) { + Icon( + imageVector = Icons.Outlined.Menu, + contentDescription = null, + ) + } + }, + actions = { + IconButton(onClick = onClose) { + Icon( + imageVector = Icons.Outlined.Close, + contentDescription = null, + ) + } + }, + ) + }, + content = { paddingValues -> + Box( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues), + ) { + RollDice( + modifier = Modifier + .align(alignment = Alignment.Center) + .padding(horizontal = 16.dp), + dice = dice, + onDice = onDice, + ) - IconButton( - modifier = Modifier - .align(alignment = Alignment.TopEnd) - .padding( - top = 6.dp, - end = 4.dp - ), - onClick = onClose - ) { - Icon( - imageVector = Icons.Outlined.Close, - contentDescription = null, + AnimatedContent( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter) + .padding(all = 16.dp), + targetState = card.value, + transitionSpec = { + val enter = + fadeIn() + slideInVertically { with(density) { 64.dp.roundToPx() } } + val exit = + fadeOut() + slideOutVertically { with(density) { -64.dp.roundToPx() } } + val transform = SizeTransform(clip = false) + enter togetherWith exit using transform + }, + label = "RollOverlayDisplay", + ) { + when (it) { + null -> Box( + modifier = Modifier.fillMaxWidth(), + ) + + else -> ThrowsCard( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onCard), + throws = it, + showDetail = showDetail, + ) + } + } + } + } ) - } - } + }, + ) } @Composable @Preview(uiMode = UI_MODE_NIGHT_NO) @Preview(uiMode = UI_MODE_NIGHT_YES) -private fun RollOverlayPreview() { +private fun RollOverlayPreview( + @PreviewParameter(RollOverlayPreviewProvider::class) preview: DrawerValue +) { LexiconTheme { Surface { RollOverlayContent( modifier = Modifier.fillMaxSize(), + drawer = rememberDrawerState(initialValue = preview), dice = remember { mutableStateOf( RollDiceUio( @@ -183,17 +266,30 @@ private fun RollOverlayPreview() { ) ) }, + alterations = remember { + mutableStateOf( + listOf( + RollAlterationUio(label = "Critique", checked = false), + ) + ) + }, showDetail = remember { mutableStateOf(true) }, + onMenu = { }, + onClose = { }, onDice = { }, onCard = { }, - onClose = { }, + onAlteration = { }, ) } } } +private class RollOverlayPreviewProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf(DrawerValue.Closed, DrawerValue.Open) +} + @Stable interface BlurredRollOverlayHostState : BlurredOverlayHostState { fun prepareRoll(roll: Roll) diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/RollOverlayViewModel.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/RollOverlayViewModel.kt index 205835a..e14212d 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/RollOverlayViewModel.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/RollOverlayViewModel.kt @@ -1,12 +1,21 @@ package com.pixelized.rplexicon.ui.screens.rolls +import android.app.Application import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.pixelized.rplexicon.R +import com.pixelized.rplexicon.model.Alteration +import com.pixelized.rplexicon.model.CharacterSheet import com.pixelized.rplexicon.model.Roll +import com.pixelized.rplexicon.repository.data.AlterationRepository +import com.pixelized.rplexicon.repository.data.CharacterSheetRepository +import com.pixelized.rplexicon.ui.screens.rolls.composable.RollAlterationUio import com.pixelized.rplexicon.ui.screens.rolls.composable.RollDiceUio import com.pixelized.rplexicon.ui.screens.rolls.composable.ThrowsCardUio +import com.pixelized.rplexicon.utilitary.extentions.context import com.pixelized.rplexicon.utilitary.extentions.icon import com.pixelized.rplexicon.utilitary.extentions.switch import dagger.hilt.android.lifecycle.HiltViewModel @@ -17,11 +26,18 @@ import javax.inject.Inject @HiltViewModel class RollOverlayViewModel @Inject constructor( + application: Application, + private val characterSheetRepository: CharacterSheetRepository, private val factory: ConvertRollIntoDisplayableFactory, -) : ViewModel() { +) : AndroidViewModel(application) { + private val criticalLabel = context.getString(R.string.dice_roll_critical_label) private var rollJob: Job? = null private lateinit var roll: Roll + private var character: CharacterSheet? = null + + private val _alterations = mutableStateOf>(emptyList()) + val alterations: State> get() = _alterations private val _dice = mutableStateOf(RollDiceUio()) val dice: State get() = _dice @@ -34,14 +50,38 @@ class RollOverlayViewModel @Inject constructor( fun prepareRoll(roll: Roll) { this.roll = roll + this.character = characterSheetRepository.find(name = roll.character) + _dice.value = RollDiceUio(icon = roll.dices.maxOf { it.faces }.icon) _card.value = null + + _alterations.value = if (roll.canUseCriticalDice) { + listOf(RollAlterationUio(label = criticalLabel, checked = false)) + } else { + emptyList() + } + } + + fun onAlteration(id: String) { + _alterations.value = _alterations.value.toMutableList().also { alterations -> + val index = alterations.indexOfFirst { it.label == id } + val alteration = alterations[index].let { it.copy(checked = it.checked.not()) } + alterations.removeAt(index) + alterations.add(index, alteration) + } } fun roll() { rollJob?.cancel() rollJob = viewModelScope.launch { - val (dice, card) = factory.roll(roll = roll) + val isCritical = _alterations.value.firstOrNull { it.label == criticalLabel }?.checked ?: false + val (dice, card) = factory.roll( + roll = roll, + diceMultiplier = when { + isCritical -> character?.criticalModifier ?: 2 + else -> 1 + }, + ) // Start the roll animation. _dice.value = _dice.value.copy( animationSpec = RollDiceUio.TWEEN, diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/composable/RollAlteration.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/composable/RollAlteration.kt new file mode 100644 index 0000000..4bc59f6 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/composable/RollAlteration.kt @@ -0,0 +1,79 @@ +package com.pixelized.rplexicon.ui.screens.rolls.composable + +import android.content.res.Configuration.UI_MODE_NIGHT_NO +import android.content.res.Configuration.UI_MODE_NIGHT_YES +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.pixelized.rplexicon.ui.theme.LexiconTheme + +@Stable +data class RollAlterationUio( + val label: String, + val checked: Boolean +) + +@Composable +fun RollAlteration( + modifier: Modifier = Modifier, + alteration: RollAlterationUio, + onClick: (id: String) -> Unit, +) { + Row( + modifier = modifier + .clickable { onClick(alteration.label) } + .padding(horizontal = 16.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + style = MaterialTheme.typography.bodyMedium, + text = alteration.label, + ) + + Switch( + enabled = true, + checked = alteration.checked, + onCheckedChange = { onClick(alteration.label) }, + ) + } +} + +@Composable +@Preview(uiMode = UI_MODE_NIGHT_NO) +@Preview(uiMode = UI_MODE_NIGHT_YES) +private fun RollAlterationPreview( + @PreviewParameter(RollAlterationPreviewProvider::class) preview: RollAlterationUio, +) { + LexiconTheme { + Surface { + RollAlteration( + modifier = Modifier.fillMaxWidth(), + alteration = preview, + onClick = { }, + ) + } + } +} + +private class RollAlterationPreviewProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf( + RollAlterationUio(label = "Critique", checked = false), + RollAlterationUio(label = "Rage", checked = true), + RollAlterationUio(label = "Bénédiction", checked = false), + ) +} \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index da2989d..4214dc8 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -127,6 +127,7 @@ Jet de dommage : \"%1$s\" Jet de sort : \"%1$s" Jet de dommage : \"%1$s\" + Critique JET DE SAUVEGARDE : %1$s Sauvegarde de \"%1$s\" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ef4e9a3..c1458ec 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -127,6 +127,7 @@ %1$s DAMAGE %1$s HIT %1$s DAMAGE + Critical %1$s SAVING THROW %1$s save