diff --git a/app/src/main/java/com/pixelized/rplexicon/data/repository/character/AlterationRepository.kt b/app/src/main/java/com/pixelized/rplexicon/data/repository/character/AlterationRepository.kt index 519dece..725ae91 100644 --- a/app/src/main/java/com/pixelized/rplexicon/data/repository/character/AlterationRepository.kt +++ b/app/src/main/java/com/pixelized/rplexicon/data/repository/character/AlterationRepository.kt @@ -68,7 +68,7 @@ class AlterationRepository @Inject constructor( * get all [Alteration] for a character * @return a list of alterations. */ - fun getAssignedAlterations(character: String): Flow> { + fun getAssignedAlterations(character: String?): Flow> { return assignedAlterations.map { alterations -> alterations[character] ?: emptyList() } @@ -81,8 +81,8 @@ class AlterationRepository @Inject constructor( * @return a list of alterations. */ fun getAssignedAlterations( - character: String, - vararg properties: Property + character: String?, + properties: List, ): Flow> { return getAssignedAlterations(character = character).map { alterations -> alterations.filter { it.status.keys.any { key -> properties.contains(key) } } 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 ab0d9a2..27f6a83 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 @@ -86,6 +86,8 @@ 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.screens.rolls.composable.rememberSlideInOutAnimation +import com.pixelized.rplexicon.ui.screens.rolls.composable.slideInOutAnimation import com.pixelized.rplexicon.ui.screens.rolls.preview.rememberRollAlterations import com.pixelized.rplexicon.ui.theme.LexiconTheme import com.pixelized.rplexicon.utilitary.extentions.lexicon @@ -167,13 +169,9 @@ private fun RollOverlayContent( onAlteration: (id: String) -> Unit, onThrowVisibilityChange: (Boolean) -> Unit, ) { - val density = LocalDensity.current val enableDrawer = remember { - derivedStateOf { - groups.value.isNotEmpty() - } + derivedStateOf { groups.value.isNotEmpty() } } - ModalNavigationDrawer( modifier = modifier, drawerState = drawer, @@ -279,9 +277,9 @@ private fun RollOverlayContent( Box( modifier = Modifier .clickable( - onClick = onClose, interactionSource = remember { MutableInteractionSource() }, indication = null, + onClick = onClose, ) .fillMaxSize() .padding(paddingValues), @@ -298,6 +296,17 @@ private fun RollOverlayContent( criticalFailure = stringResource(id = R.string.roll_overlay__critical_failure), ) + Card( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter) + .padding(bottom = if (enableDrawer.value) 32.dp else 0.dp), + card = card, + isDarkTheme = isDarkTheme, + showDetail = showDetail, + onCard = onCard + ) + AnimatedVisibility( modifier = Modifier.align(alignment = Alignment.BottomEnd), visible = enableDrawer.value, @@ -309,33 +318,6 @@ private fun RollOverlayContent( ) } } - - AnimatedContent( - modifier = Modifier - .fillMaxWidth() - .align(Alignment.BottomCenter), - targetState = card.value, - transitionSpec = { - animation(density = density) using SizeTransform(clip = false) - }, - label = "RollOverlayDisplay", - ) { - when (it) { - null -> Box( - modifier = Modifier.fillMaxWidth(), - ) - - else -> ThrowsCard( - modifier = Modifier - .padding(bottom = if (enableDrawer.value) 32.dp else 0.dp) - .padding(all = 16.dp), - isDarkTheme = isDarkTheme, - throws = it, - showDetail = showDetail, - onClick = onCard, - ) - } - } } } ) @@ -343,6 +325,33 @@ private fun RollOverlayContent( ) } +@Composable +private fun Card( + modifier: Modifier = Modifier, + card: State, + isDarkTheme: Boolean, + showDetail: State, + onCard: () -> Unit +) { + AnimatedContent( + modifier = modifier, + targetState = card.value, + transitionSpec = rememberSlideInOutAnimation(), + label = "RollOverlayDisplay", + ) { + when (it) { + null -> Box(modifier = Modifier.fillMaxWidth()) + else -> ThrowsCard( + modifier = Modifier.padding(all = 16.dp), + isDarkTheme = isDarkTheme, + throws = it, + showDetail = showDetail, + onClick = onCard, + ) + } + } +} + private fun Modifier.detailPaddingBottom( showDetail: State, bottom: Dp = 128.dp, @@ -459,12 +468,6 @@ private class RollOverlayPreviewProvider : PreviewParameterProvider(null) + + private val _alterationsOverride = MutableStateFlow>(emptyMap()) + private val _alterations: StateFlow> = combine( + _diceThrow, + descriptionRepository.data, + activeAlterationRepository.getActiveAlterations(), + _alterationsOverride, + ) { diceThrow, descriptions, actives, override -> + alterationFactory.convertThrowAlterationItem( + diceThrow = diceThrow, + description = descriptions, + checked = actives[diceThrow?.character]?.associate { it.name to true } ?: emptyMap(), + override = override, + ) + }.stateIn( + scope = viewModelScope, + started = SharingStarted.Lazily, + initialValue = emptyList(), + ) + + private val _characterSheet: StateFlow = combine( + characterSheetRepository.data, + _diceThrow, + ) { sheets, diceThrow -> + sheets[diceThrow?.character] + }.stateIn( + scope = viewModelScope, + started = SharingStarted.Lazily, + initialValue = null, + ) - private val _alterations = MutableStateFlow>(emptyList()) - private val _alterationStatusOverride = MutableStateFlow>(emptyMap()) val alterations: State> @Composable - get() = _alterations.map { data -> - data.groupBy { it.source } + @Stable + get() = combine(_characterSheet, _alterations) { sheet, alterations -> + alterations.groupBy { it.source } .toSortedMap(AlterationSortUseCase.sort(sheet = sheet)) .map { entry -> AlterationGroupUio( @@ -80,7 +108,8 @@ class RollOverlayViewModel @Inject constructor( } }.collectAsState(initial = emptyList()) - private var targetRoll = 0f + private var _rollJob: Job? = null + private var _targetRoll = 0f private val _dice = mutableStateOf(null) val dice: State get() = _dice val diceRotation: Animatable = Animatable(0f) @@ -98,83 +127,56 @@ class RollOverlayViewModel @Inject constructor( val alterationDetailDialog: State get() = _alterationDetailDialog suspend fun prepareRoll(diceThrow: DiceThrow) { - this.targetRoll = 0f - this.diceThrow = diceThrow + // save the dice throw. + _diceThrow.value = diceThrow + // reinitialise the roll parameter + _targetRoll = 0f + _rollJob?.cancel() + diceRotation.snapTo(_targetRoll) // hide the throw for other player. _isThrowHidden.value = diceThrow is DiceThrow.DeathSavingThrow // hide the detail throw card. _card.value = null // reset the override status - _alterationStatusOverride.value = emptyMap() - // get the character sheet. - sheet = characterSheetRepository.find( - name = diceThrow.character, - ) + _alterationsOverride.value = emptyMap() // build the dice UIO. - _dice.value = diceFactory.convertDiceThrow( - diceThrow = diceThrow, - ) - // listen to alteration + active to build the list of applicable alteration. - alterationJob?.cancel() - alterationJob = viewModelScope.launch(Dispatchers.Default) { - val character = diceThrow.character - val struct = AlterationStruct() - descriptionRepository.data - .combine(activeAlterationRepository.getActiveAssignedAlterations(character = character)) { descriptions, actives -> - struct.descriptions = descriptions - struct.actives = actives.associate { it.name to true } - } - .combine(_alterationStatusOverride) { _, override -> - struct.override = override - } - .collect { - val (description, actives, override) = struct - val alterations = alterationFactory.convertThrowAlterationItem( - diceThrow = diceThrow, - description = description, - checked = actives, - override = override, - ) - withContext(Dispatchers.Main) { - _alterations.value = alterations - } - } + _dice.value = withContext(Dispatchers.Default) { + diceFactory.convertDiceThrow(diceThrow = diceThrow) } } - fun onAlteration(id: String) { - val override = _alterationStatusOverride.value.toMutableMap().also { - it[id] = it[id]?.not() ?: firebaseRepository.getAlterationStatusSnapshot( - key = AlterationStatus.Key(character = diceThrow.character, alteration = id) - ).value.not() - } - _alterationStatusOverride.value = override - } - fun roll(uiScope: CoroutineScope) { - rollJob?.cancel() - rollJob = uiScope.launch { + _rollJob?.cancel() + _rollJob = uiScope.launch { // roll the dice ;) - val result = rollUseCase.roll( - diceThrow = diceThrow, - isThrowHidden = _isThrowHidden.value, - alterationId = _alterations.value.mapNotNull { if (it.checked) it.label else null }, - ) + val result = withContext(Dispatchers.Default) { + _diceThrow.value?.let { + rollUseCase.roll( + diceThrow = it, + isThrowHidden = _isThrowHidden.value, + alterationId = _alterations.value.mapNotNull { alteration -> + if (alteration.checked) alteration.label else null + }, + ) + } + } if (result != null) { // reset the displayed dice value - _dice.value = result.dice.copy( - result = null, - isCriticalSuccess = false, - isCriticalFailure = false, - ) + _dice.value = withContext(Dispatchers.Default) { + result.dice.copy( + result = null, + isCriticalSuccess = false, + isCriticalFailure = false, + ) + } // play the roll animation + _targetRoll += 720f launch { - targetRoll += 360f * 4 diceRotation.animateTo( - targetValue = targetRoll, + targetValue = _targetRoll, animationSpec = spring( - dampingRatio = 0.9f, - stiffness = 105f, + dampingRatio = 0.8f, + stiffness = 15f, ), ) } @@ -184,9 +186,17 @@ class RollOverlayViewModel @Inject constructor( if (isActive) { _dice.value = result.dice // share the result - throwRepository.sendThrow(sheet?.name, result.throws) - // Display the roll card. - _card.value = throwCardFactory.convert(result.throws) + withContext(Dispatchers.IO) { + throwRepository.sendThrow(_characterSheet.value?.name, result.throws) + } + } + // Wait a fix amount of time then + delay(RollDiceUio.ROLL_DURATION.toLong() + 250L) + // Display the roll card. + if (isActive) { + _card.value = withContext(Dispatchers.Default) { + throwCardFactory.convert(result.throws) + } } } } @@ -200,9 +210,21 @@ class RollOverlayViewModel @Inject constructor( _isThrowHidden.value = hidden } - suspend fun showAlterationDetail(id: String) { + fun onAlteration(id: String) { + val override = _alterationsOverride.value.toMutableMap().also { + it[id] = it[id]?.not() ?: firebaseRepository.getAlterationStatusSnapshot( + key = AlterationStatus.Key( + character = _diceThrow.value?.character ?: "", + alteration = id + ) + ).value.not() + } + _alterationsOverride.value = override + } + + fun showAlterationDetail(id: String) { val alteration = alterationRepository.getAssignedAlterationSnapshot( - character = diceThrow.character, + character = _diceThrow.value?.character, alteration = id, ) val description = descriptionRepository.getDescription( @@ -224,10 +246,4 @@ class RollOverlayViewModel @Inject constructor( fun hideAlterationDetail() { _alterationDetailDialog.value = null } - - private data class AlterationStruct( - var descriptions: Map = emptyMap(), - var actives: Map = emptyMap(), - var override: Map = emptyMap(), - ) } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/composable/RollDice.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/composable/RollDice.kt index 3cad725..91a9438 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/composable/RollDice.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/composable/RollDice.kt @@ -1,18 +1,14 @@ package com.pixelized.rplexicon.ui.screens.rolls.composable +import android.content.res.Configuration import androidx.annotation.DrawableRes import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.SizeTransform import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.AnimationVector1D import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween -import androidx.compose.animation.expandIn import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut -import androidx.compose.animation.shrinkOut -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutVertically import androidx.compose.animation.togetherWith import androidx.compose.foundation.Image import androidx.compose.foundation.clickable @@ -43,7 +39,6 @@ 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.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import com.pixelized.rplexicon.R import com.pixelized.rplexicon.ui.agsl.dancingColor @@ -59,7 +54,7 @@ data class RollDiceUio( val isCriticalFailure: Boolean = false, val result: String? = null, ) { - companion object{ + companion object { const val ROLL_DURATION = 500 } } @@ -146,33 +141,18 @@ private fun Result( dice: State, ) { AnimatedContent( - modifier = modifier, targetState = dice.value?.result, - transitionSpec = { - val enter = fadeIn() + - expandIn(expandFrom = Alignment.Center) { it } + - slideInVertically { it / 5 } - val exit = fadeOut() + - shrinkOut(shrinkTowards = Alignment.Center) { IntSize.Zero } + - slideOutVertically { -it / 5 } - enter togetherWith exit using SizeTransform(clip = false) - }, + transitionSpec = rememberSlideInOutAnimation(), label = "ResultAnimation" ) { - when (it) { - null -> Box( - modifier = Modifier, + Box( + modifier = modifier, + contentAlignment = Alignment.Center, + ) { + Text( + style = MaterialTheme.lexicon.typography.diceRoll, + text = it ?: "", ) - - else -> Box( - modifier = Modifier, - contentAlignment = Alignment.Center, - ) { - Text( - style = MaterialTheme.lexicon.typography.diceRoll, - text = it, - ) - } } } } @@ -221,7 +201,8 @@ private fun Critical( } @Composable -@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) private fun SkillRollPreview( @PreviewParameter(SkillRollPreviewProvider::class) preview: Int, ) { diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/composable/slideInOutAnimation.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/composable/slideInOutAnimation.kt new file mode 100644 index 0000000..75d7a79 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/composable/slideInOutAnimation.kt @@ -0,0 +1,30 @@ +package com.pixelized.rplexicon.ui.screens.rolls.composable + +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.ContentTransform +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.animation.togetherWith +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.dp + +@Composable +@Stable +fun rememberSlideInOutAnimation(): AnimatedContentTransitionScope.() -> ContentTransform { + val density = LocalDensity.current + return remember(density) { + { slideInOutAnimation(density = density) } + } +} + +fun slideInOutAnimation(density: Density): ContentTransform { + val enter = fadeIn() + slideInVertically { with(density) { 64.dp.roundToPx() } } + val exit = fadeOut() + slideOutVertically { with(density) { -64.dp.roundToPx() } } + return enter togetherWith exit +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/factory/AlterationFactory.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/factory/AlterationFactory.kt index b6f9cad..ceb5521 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/factory/AlterationFactory.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/factory/AlterationFactory.kt @@ -36,7 +36,7 @@ class AlterationFactory @Inject constructor( } suspend fun convertThrowAlterationItem( - diceThrow: DiceThrow, + diceThrow: DiceThrow?, description: Map, checked: Map, override: Map, @@ -139,10 +139,12 @@ class AlterationFactory @Inject constructor( } is DiceThrow.Object -> listOf(Property.OBJECT_EFFECT) + + null -> emptyList() } return alterationRepository - .getAssignedAlterations(character = diceThrow.character, *properties.toTypedArray()) + .getAssignedAlterations(character = diceThrow?.character, properties) .firstOrNull() ?.map { alteration -> AlterationItemUio( diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/theme/typography/LexiconTypography.kt b/app/src/main/java/com/pixelized/rplexicon/ui/theme/typography/LexiconTypography.kt index 25bcd3a..e5ad94b 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/theme/typography/LexiconTypography.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/theme/typography/LexiconTypography.kt @@ -187,10 +187,7 @@ fun lexiconTypography( ), diceRoll: TextStyle = base.displayMedium.copy( shadow = when (isDarkTheme) { - true -> Shadow( - offset = Offset(4f, 4f), - blurRadius = 8f, - ) + true -> Shadow(blurRadius = 12f) else -> null } ),