Rework a bit the dice throw animation.

This commit is contained in:
Andres Gomez, Thomas (ITDV RL) 2024-08-08 17:27:07 +02:00
parent 248a51f396
commit aeb8eaf9ff
7 changed files with 192 additions and 163 deletions

View file

@ -68,7 +68,7 @@ class AlterationRepository @Inject constructor(
* get all [Alteration] for a character
* @return a list of alterations.
*/
fun getAssignedAlterations(character: String): Flow<List<Alteration>> {
fun getAssignedAlterations(character: String?): Flow<List<Alteration>> {
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<Property>,
): Flow<List<Alteration>> {
return getAssignedAlterations(character = character).map { alterations ->
alterations.filter { it.status.keys.any { key -> properties.contains(key) } }

View file

@ -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<ThrowsCardUio?>,
isDarkTheme: Boolean,
showDetail: State<Boolean>,
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<Boolean>,
bottom: Dp = 128.dp,
@ -459,12 +468,6 @@ private class RollOverlayPreviewProvider : PreviewParameterProvider<RollOverlayP
)
}
private fun animation(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
}
@Stable
interface BlurredRollOverlayHostState : BlurredOverlayHostState {
suspend fun prepareRoll(diceThrow: DiceThrow)

View file

@ -5,6 +5,7 @@ import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationVector1D
import androidx.compose.animation.core.spring
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateOf
@ -14,7 +15,6 @@ import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.business.AlterationSortUseCase
import com.pixelized.rplexicon.business.DiceThrowUseCase
import com.pixelized.rplexicon.data.model.CharacterSheet
import com.pixelized.rplexicon.data.model.Description
import com.pixelized.rplexicon.data.model.DiceThrow
import com.pixelized.rplexicon.data.model.alteration.AlterationStatus
import com.pixelized.rplexicon.data.repository.character.ActiveAlterationRepository
@ -39,8 +39,10 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -48,9 +50,7 @@ import javax.inject.Inject
@HiltViewModel
class RollOverlayViewModel @Inject constructor(
private val characterSheetRepository: CharacterSheetRepository,
private val alterationRepository: AlterationRepository,
private val activeAlterationRepository: ActiveAlterationRepository,
private val descriptionRepository: DescriptionRepository,
private val rollUseCase: DiceThrowUseCase,
private val diceFactory: DiceFactory,
@ -58,19 +58,47 @@ class RollOverlayViewModel @Inject constructor(
private val firebaseRepository: CharacterFireSheetRepository,
private val throwRepository: NetworkThrowFireRepository,
private val throwCardFactory: ThrowCardFactory,
characterSheetRepository: CharacterSheetRepository,
activeAlterationRepository: ActiveAlterationRepository,
application: Application,
) : AndroidViewModel(application) {
private var sheet: CharacterSheet? = null
private var rollJob: Job? = null
private var alterationJob: Job? = null
private lateinit var diceThrow: DiceThrow
private val _diceThrow = MutableStateFlow<DiceThrow?>(null)
private val _alterationsOverride = MutableStateFlow<Map<String, Boolean>>(emptyMap())
private val _alterations: StateFlow<List<AlterationItemUio>> = 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<CharacterSheet?> = combine(
characterSheetRepository.data,
_diceThrow,
) { sheets, diceThrow ->
sheets[diceThrow?.character]
}.stateIn(
scope = viewModelScope,
started = SharingStarted.Lazily,
initialValue = null,
)
private val _alterations = MutableStateFlow<List<AlterationItemUio>>(emptyList())
private val _alterationStatusOverride = MutableStateFlow<Map<String, Boolean>>(emptyMap())
val alterations: State<List<AlterationGroupUio>>
@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<RollDiceUio?>(null)
val dice: State<RollDiceUio?> get() = _dice
val diceRotation: Animatable<Float, AnimationVector1D> = Animatable(0f)
@ -98,83 +127,56 @@ class RollOverlayViewModel @Inject constructor(
val alterationDetailDialog: State<AlterationDialogDetailUio?> 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<String, Description> = emptyMap(),
var actives: Map<String, Boolean> = emptyMap(),
var override: Map<String, Boolean> = emptyMap(),
)
}

View file

@ -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<RollDiceUio?>,
) {
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,
) {

View file

@ -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 <S> rememberSlideInOutAnimation(): AnimatedContentTransitionScope<S>.() -> 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
}

View file

@ -36,7 +36,7 @@ class AlterationFactory @Inject constructor(
}
suspend fun convertThrowAlterationItem(
diceThrow: DiceThrow,
diceThrow: DiceThrow?,
description: Map<String, Description>,
checked: Map<String, Boolean>,
override: Map<String, Boolean>,
@ -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(

View file

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