From ac71765c44aa6ede7381f3a658ea6ce7baef6c48 Mon Sep 17 00:00:00 2001 From: "Andres Gomez, Thomas (ITDV RL)" Date: Tue, 20 Aug 2024 14:44:52 +0200 Subject: [PATCH] Add the "know what you roll" feature, allow automation after a roll. --- .../com/pixelized/rplexicon/MainActivity.kt | 33 +-- .../rplexicon/data/model/item/Item.kt | 2 +- .../parser/inventory/ItemLexiconParser.kt | 6 +- .../ui/composable/BlurredOverlayHost.kt | 101 -------- .../screens/character/CharacterSheetScreen.kt | 23 +- .../character/pages/actions/ActionsPage.kt | 18 +- .../pages/actions/HeaderViewModel.kt | 14 +- .../pages/inventory/InventoryPage.kt | 26 ++- .../pages/inventory/InventoryViewModel.kt | 19 +- .../pages/inventory_OLD/InventoryPageOLD.kt | 3 +- .../pages/proficiency/ProficiencyPage.kt | 9 +- .../screens/rolls/BlurredRollOverlayHost.kt | 216 ++++++++++++++++++ .../rplexicon/ui/screens/rolls/RollOverlay.kt | 56 +---- .../ui/screens/rolls/RollOverlayViewModel.kt | 10 + 14 files changed, 312 insertions(+), 224 deletions(-) delete mode 100644 app/src/main/java/com/pixelized/rplexicon/ui/composable/BlurredOverlayHost.kt create mode 100644 app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/BlurredRollOverlayHost.kt diff --git a/app/src/main/java/com/pixelized/rplexicon/MainActivity.kt b/app/src/main/java/com/pixelized/rplexicon/MainActivity.kt index e65af57..1f55554 100644 --- a/app/src/main/java/com/pixelized/rplexicon/MainActivity.kt +++ b/app/src/main/java/com/pixelized/rplexicon/MainActivity.kt @@ -2,6 +2,7 @@ package com.pixelized.rplexicon import android.app.Activity import android.os.Bundle +import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.BackHandler import androidx.activity.compose.setContent @@ -30,13 +31,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.WindowCompat -import com.pixelized.rplexicon.ui.composable.BlurredOverlayHost import com.pixelized.rplexicon.ui.composable.error.HandleFetchError import com.pixelized.rplexicon.ui.navigation.ScreenNavHost -import com.pixelized.rplexicon.ui.screens.rolls.BlurredRollOverlayHostState -import com.pixelized.rplexicon.ui.screens.rolls.RollOverlay +import com.pixelized.rplexicon.ui.screens.rolls.BlurredOverlayHostState +import com.pixelized.rplexicon.ui.screens.rolls.BlurredRollOverlayHost import com.pixelized.rplexicon.ui.screens.rolls.RollOverlayViewModel -import com.pixelized.rplexicon.ui.screens.rolls.rememberBlurredRollOverlayHostState +import com.pixelized.rplexicon.ui.screens.rolls.rememberBlurredOverlayHostState import com.pixelized.rplexicon.ui.theme.LexiconTheme import com.pixelized.rplexicon.utilitary.extentions.modifier.doubleBorder import dagger.hilt.android.AndroidEntryPoint @@ -49,7 +49,7 @@ val LocalActivity = compositionLocalOf { val LocalSnack = compositionLocalOf { error("SnackbarHostState not available") } -val LocalRollOverlay = compositionLocalOf { +val LocalRollOverlay = compositionLocalOf { error("LocalRollOverlay not yet ready") } @@ -73,8 +73,15 @@ class MainActivity : ComponentActivity() { setContent { val snack = remember { SnackbarHostState() } - val overlay = rememberBlurredRollOverlayHostState( - viewModel = rollViewModel, + val overlay = rememberBlurredOverlayHostState( + onPrepareRoll = { dice -> + dice?.let { + rollViewModel.prepareRoll(diceThrow = it) + } + }, + onShowOverlay = { data -> + rollViewModel.setRollOverlayData(data) + } ) CompositionLocalProvider( @@ -94,13 +101,9 @@ class MainActivity : ComponentActivity() { .padding(paddingValues = padding), color = MaterialTheme.colorScheme.background ) { - BlurredOverlayHost( - rollOverlayState = overlay, - overlay = { - RollOverlay( - viewModel = rollViewModel, - ) - }, + BlurredRollOverlayHost( + rollViewModel = rollViewModel, + state = overlay, content = { ScreenNavHost() }, @@ -150,7 +153,7 @@ class MainActivity : ComponentActivity() { BackHandler( enabled = overlay.isOverlayVisible, - onBack = { overlay.hideOverlay() }, + onBack = { overlay.dismiss() }, ) HandleFetchError( diff --git a/app/src/main/java/com/pixelized/rplexicon/data/model/item/Item.kt b/app/src/main/java/com/pixelized/rplexicon/data/model/item/Item.kt index 73cadf8..fcef2f3 100644 --- a/app/src/main/java/com/pixelized/rplexicon/data/model/item/Item.kt +++ b/app/src/main/java/com/pixelized/rplexicon/data/model/item/Item.kt @@ -11,7 +11,7 @@ data class Item( val context: String?, val isContainer: Boolean, val effect: Throw?, - val usable: Boolean, + val consumable: Boolean, val icon: Uri?, ) { val fullName: String = prefix?.let { "$it${name}" } ?: name diff --git a/app/src/main/java/com/pixelized/rplexicon/data/parser/inventory/ItemLexiconParser.kt b/app/src/main/java/com/pixelized/rplexicon/data/parser/inventory/ItemLexiconParser.kt index 7134ced..20f5eda 100644 --- a/app/src/main/java/com/pixelized/rplexicon/data/parser/inventory/ItemLexiconParser.kt +++ b/app/src/main/java/com/pixelized/rplexicon/data/parser/inventory/ItemLexiconParser.kt @@ -31,7 +31,7 @@ class ItemLexiconParser @Inject constructor( type = row.parse(column = TYPE), context = row.parse(column = CONTEXT), isContainer = row.parseBool(column = CONTAINER) ?: false, - usable = row.parseBool(column = USABLE) ?: false, + consumable = row.parseBool(column = CONSUMABLE) ?: false, effect = throwParser.parse(value = row.parse(column = EFFECT)), icon = row.parseUri(column = ICON), ) @@ -51,11 +51,11 @@ class ItemLexiconParser @Inject constructor( private val TYPE = column("Type") private val CONTEXT = column("Contexte") private val CONTAINER = column("Contenant") - private val USABLE = column("Utilisable") + private val CONSUMABLE = column("Consommable") private val EFFECT = column("Effet") private val ICON = column("Icone") private val COLUMNS = - listOf(ID, PREFIX, NAME, TYPE, CONTEXT, CONTAINER, USABLE, EFFECT, ICON) + listOf(ID, PREFIX, NAME, TYPE, CONTEXT, CONTAINER, CONSUMABLE, EFFECT, ICON) } } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/composable/BlurredOverlayHost.kt b/app/src/main/java/com/pixelized/rplexicon/ui/composable/BlurredOverlayHost.kt deleted file mode 100644 index 761b890..0000000 --- a/app/src/main/java/com/pixelized/rplexicon/ui/composable/BlurredOverlayHost.kt +++ /dev/null @@ -1,101 +0,0 @@ -package com.pixelized.rplexicon.ui.composable - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.animateDpAsState -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutVertically -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.systemBarsPadding -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.Stable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.BlurredEdgeTreatment -import androidx.compose.ui.draw.blur -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.dp -import com.pixelized.rplexicon.utilitary.extentions.lexicon -import com.pixelized.rplexicon.utilitary.extentions.modifier.clickableInterceptor - -@Stable -interface BlurredOverlayHostState { - val isOverlayVisible: Boolean - fun showOverlay() - fun hideOverlay() -} - -@Stable -private class BlurredOverlayHostStateImpl( - rollOverlayVisibilityState: MutableState, -) : BlurredOverlayHostState { - override var isOverlayVisible by rollOverlayVisibilityState - - override fun showOverlay() { - isOverlayVisible = true - } - - override fun hideOverlay() { - isOverlayVisible = false - } -} - -@Composable -@Stable -fun rememberBlurredOverlayHostState(): BlurredOverlayHostState { - val rollOverlayVisibilityState = rememberSaveable { mutableStateOf(false) } - return remember { - BlurredOverlayHostStateImpl( - rollOverlayVisibilityState = rollOverlayVisibilityState - ) - } -} - -@Composable -fun BlurredOverlayHost( - rollOverlayState: BlurredOverlayHostState = rememberBlurredOverlayHostState(), - overlay: @Composable () -> Unit, - content: @Composable () -> Unit, -) { - val density = LocalDensity.current - - Surface { - val blurs = animateDpAsState( - targetValue = if (rollOverlayState.isOverlayVisible) 4.dp else 0.dp, - label = "RollOverlayHostBlurAnimation", - ) - Box( - modifier = Modifier.blur( - radius = blurs.value, - edgeTreatment = BlurredEdgeTreatment.Unbounded, - ), - content = { content() }, - ) - AnimatedVisibility( - visible = rollOverlayState.isOverlayVisible, - enter = fadeIn() + slideInVertically { with(density) { 64.dp.roundToPx() } }, - exit = fadeOut() + slideOutVertically { with(density) { 64.dp.roundToPx() } }, - content = { - Box( - modifier = Modifier - .clickableInterceptor() - .background(color = MaterialTheme.lexicon.colorScheme.rollOverlayBrush) - .fillMaxSize() - .systemBarsPadding(), - ) { - overlay() - } - }, - ) - } -} \ No newline at end of file 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 0b8e3fb..4a39699 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 @@ -59,6 +59,7 @@ import com.pixelized.rplexicon.NO_WINDOW_INSETS import com.pixelized.rplexicon.R import com.pixelized.rplexicon.ui.composable.KeepOnScreen import com.pixelized.rplexicon.ui.composable.Loader +import com.pixelized.rplexicon.ui.screens.rolls.RollResult import com.pixelized.rplexicon.ui.composable.Toolbar import com.pixelized.rplexicon.ui.composable.edit.HandleHitPointEditDialog import com.pixelized.rplexicon.ui.composable.edit.HandleSkillEditDialog @@ -83,9 +84,9 @@ import com.pixelized.rplexicon.ui.screens.character.pages.actions.SpellsViewMode import com.pixelized.rplexicon.ui.screens.character.pages.alteration.AlterationPage import com.pixelized.rplexicon.ui.screens.character.pages.alteration.AlterationPagePreview import com.pixelized.rplexicon.ui.screens.character.pages.alteration.AlterationViewModel -import com.pixelized.rplexicon.ui.screens.character.pages.inventory_OLD.InventoryViewModelOLD import com.pixelized.rplexicon.ui.screens.character.pages.inventory.InventoryPage2 import com.pixelized.rplexicon.ui.screens.character.pages.inventory.InventoryPreview +import com.pixelized.rplexicon.ui.screens.character.pages.inventory_OLD.InventoryViewModelOLD import com.pixelized.rplexicon.ui.screens.character.pages.proficiency.ProficiencyPage import com.pixelized.rplexicon.ui.screens.character.pages.proficiency.ProficiencyPreview import com.pixelized.rplexicon.ui.screens.character.pages.proficiency.ProficiencyViewModel @@ -149,8 +150,7 @@ fun CharacterSheetScreen( }, onInitiative = { scope.launch { - overlay.prepareRoll(diceThrow = headerViewModel.initiativeRoll()) - overlay.showOverlay() + overlay.showOverlay(diceThrow = headerViewModel.initiativeRoll()) } }, onHitPoint = headerViewModel::toggleHitPointDialog, @@ -159,8 +159,16 @@ fun CharacterSheetScreen( }, onDeathRoll = { scope.launch { - overlay.prepareRoll(diceThrow = headerViewModel.onDeathThrow()) - overlay.showOverlay() + val dices = headerViewModel.onDeathThrow() + val result = overlay.showOverlay(diceThrow = dices) + if (result is RollResult.Roll) { + when { + result.isCriticalSuccess -> headerViewModel.onDeathSuccess(critical = true) + result.isCriticalFailure -> headerViewModel.onDeathFailure(critical = true) + result.value < 10 -> headerViewModel.onDeathFailure() + else -> headerViewModel.onDeathSuccess() + } + } } }, onDeathSuccess = headerViewModel::onDeathSuccess, @@ -203,13 +211,12 @@ fun CharacterSheetScreen( onLevel = { spell, level -> scope.launch { sheetState.hide() - overlay.prepareRoll( + overlay.showOverlay( diceThrow = spellsViewModel.onCastSpell( spell, level ) ) - overlay.showOverlay() } }, ) @@ -237,7 +244,7 @@ fun CharacterSheetScreen( } BackHandler(enabled = overlay.isOverlayVisible) { - overlay.hideOverlay() + overlay.dismiss() } KeepOnScreen() diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/actions/ActionsPage.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/actions/ActionsPage.kt index 46aca7a..3e8fd84 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/actions/ActionsPage.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/actions/ActionsPage.kt @@ -63,16 +63,14 @@ fun ActionPage( onAttackHit = { id -> attacksViewModel.onHitRoll(id)?.let { scope.launch { - overlay.prepareRoll(diceThrow = it) - overlay.showOverlay() + overlay.showOverlay(diceThrow = it) } } }, onAttackDamage = { id -> attacksViewModel.onDamageRoll(id)?.let { scope.launch { - overlay.prepareRoll(diceThrow = it) - overlay.showOverlay() + overlay.showOverlay(diceThrow = it) } } }, @@ -81,8 +79,7 @@ fun ActionPage( }, onSkillThrow = { scope.launch { - overlay.prepareRoll(diceThrow = skillViewModel.onSkillRoll(it.label)) - overlay.showOverlay() + overlay.showOverlay(diceThrow = skillViewModel.onSkillRoll(it.label)) } }, onSkillInfo = { @@ -100,14 +97,12 @@ fun ActionPage( }, onSpellHit = { id -> scope.launch { - overlay.prepareRoll(diceThrow = spellsViewModel.onSpellHitRoll(id)) - overlay.showOverlay() + overlay.showOverlay(diceThrow = spellsViewModel.onSpellHitRoll(id)) } }, onSpellDamage = { id -> scope.launch { - overlay.prepareRoll(diceThrow = spellsViewModel.onSpellDamageRoll(id)) - overlay.showOverlay() + overlay.showOverlay(diceThrow = spellsViewModel.onSpellDamageRoll(id)) } }, onCast = { @@ -116,8 +111,7 @@ fun ActionPage( scope.launch { sheetState.show() } } else { scope.launch { - overlay.prepareRoll(diceThrow = spellsViewModel.onCastSpell(it)) - overlay.showOverlay() + overlay.showOverlay(diceThrow = spellsViewModel.onCastSpell(it)) } } }, diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/actions/HeaderViewModel.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/actions/HeaderViewModel.kt index c226f0c..ac77796 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/actions/HeaderViewModel.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/actions/HeaderViewModel.kt @@ -113,19 +113,25 @@ class HeaderViewModel @Inject constructor( return DiceThrow.DeathSavingThrow(character = character) } - fun onDeathSuccess() { + fun onDeathSuccess( + critical: Boolean = false, + ) { + val token = if (critical) 2 else 1 firebaseRepository.setCharacterDeathCounter( character = character, - success = ((fireData.value?.deathSuccess ?: 0) + 1) % 4, + success = ((fireData.value?.deathSuccess ?: 0) + token) % 4, failure = fireData.value?.deathFailure ?: 0, ) } - fun onDeathFailure() { + fun onDeathFailure( + critical: Boolean = false, + ) { + val token = if (critical) 2 else 1 firebaseRepository.setCharacterDeathCounter( character = character, success = fireData.value?.deathSuccess ?: 0, - failure = ((fireData.value?.deathFailure ?: 0) + 1) % 4, + failure = ((fireData.value?.deathFailure ?: 0) + token) % 4, ) } diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/inventory/InventoryPage.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/inventory/InventoryPage.kt index 04f1744..64f638a 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/inventory/InventoryPage.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/inventory/InventoryPage.kt @@ -57,6 +57,7 @@ import com.pixelized.rplexicon.ui.screens.character.pages.inventory.item_detail. import com.pixelized.rplexicon.ui.screens.character.pages.inventory.item_detail.ItemDetailViewModel import com.pixelized.rplexicon.ui.screens.character.pages.inventory.item_list.ItemListDialog import com.pixelized.rplexicon.ui.screens.character.pages.inventory.item_list.ItemListViewModel +import com.pixelized.rplexicon.ui.screens.rolls.RollResult import com.pixelized.rplexicon.ui.theme.LexiconTheme import kotlinx.coroutines.launch @@ -107,11 +108,14 @@ fun InventoryPage2( }, onItemUse = { id, quantity -> // try to roll the dices. - val diceThrow = inventoryViewModel.useItem(itemId = id) - if (diceThrow != null) { - scope.launch { - overlay.prepareRoll(diceThrow = diceThrow) - overlay.showOverlay() + scope.launch { + val (item, dices) = inventoryViewModel.useItem(itemId = id) + val result = overlay.showOverlay(diceThrow = dices) + if (result is RollResult.Roll && item?.consumable == true) { + inventoryViewModel.setItemQuantity( + itemId = id, + quantity = quantity - 1 + ) } } }, @@ -141,10 +145,14 @@ fun InventoryPage2( // hide the detail dialog itemDetailViewModel.hide() // try to roll the dices. - inventoryViewModel.useItem(itemId = detail.id)?.let { diceThrow -> - scope.launch { - overlay.prepareRoll(diceThrow = diceThrow) - overlay.showOverlay() + scope.launch { + val (item, dices) = inventoryViewModel.useItem(itemId = detail.id) + val result = overlay.showOverlay(diceThrow = dices) + if (result is RollResult.Roll && item?.consumable == true) { + inventoryViewModel.setItemQuantity( + itemId = detail.id, + quantity = detail.quantity?.minus(1) ?: 0 + ) } } } diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/inventory/InventoryViewModel.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/inventory/InventoryViewModel.kt index a45667b..d53e2b0 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/inventory/InventoryViewModel.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/inventory/InventoryViewModel.kt @@ -9,6 +9,7 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.pixelized.rplexicon.data.model.DiceThrow +import com.pixelized.rplexicon.data.model.item.Item import com.pixelized.rplexicon.data.repository.character.CharacterSheetRepository import com.pixelized.rplexicon.data.repository.character.ItemsRepository import com.pixelized.rplexicon.data.repository.firebase.inventory.InventoryFireRepository @@ -329,16 +330,18 @@ class InventoryViewModel @Inject constructor( fun useItem( itemId: String, - ): DiceThrow? { + ): Pair { val item = itemRepository.find(id = itemId) - return if (item?.effect != null) { - DiceThrow.Object( - character = character, - itemId = item.id, - itemName = item.name, - ) + return if (item == null) { + null to null } else { - null + item to item.effect?.let { + DiceThrow.Object( + character = character, + itemId = item.id, + itemName = item.name, + ) + } } } } diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/inventory_OLD/InventoryPageOLD.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/inventory_OLD/InventoryPageOLD.kt index 21715a1..739957d 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/inventory_OLD/InventoryPageOLD.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/inventory_OLD/InventoryPageOLD.kt @@ -112,8 +112,7 @@ fun InventoryPageOLD( // detailViewModel.hide(detail) viewModel.onUse(itemId = detail.id)?.let { diceThrow -> scope.launch { - overlay.prepareRoll(diceThrow = diceThrow) - overlay.showOverlay() + overlay.showOverlay(diceThrow = diceThrow) } } } diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/proficiency/ProficiencyPage.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/proficiency/ProficiencyPage.kt index 7e943d3..d85ede9 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/proficiency/ProficiencyPage.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/proficiency/ProficiencyPage.kt @@ -83,14 +83,12 @@ fun ProficiencyPage( passives = viewModel.skills, onStats = { stat -> scope.launch { - overlay.prepareRoll(diceThrow = viewModel.statRoll(stat.id)) - overlay.showOverlay() + overlay.showOverlay(diceThrow = viewModel.statRoll(stat.id)) } }, onProficiencies = { proficiency -> scope.launch { - overlay.prepareRoll(diceThrow = viewModel.proficiencyRoll(proficiency.id)) - overlay.showOverlay() + overlay.showOverlay(diceThrow = viewModel.proficiencyRoll(proficiency.id)) } }, onSkillCount = { @@ -98,8 +96,7 @@ fun ProficiencyPage( }, onSkillThrow = { scope.launch { - overlay.prepareRoll(diceThrow = viewModel.onSkillRoll(it.label)) - overlay.showOverlay() + overlay.showOverlay(diceThrow = viewModel.onSkillRoll(it.label)) } }, onSkillInfo = { diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/BlurredRollOverlayHost.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/BlurredRollOverlayHost.kt new file mode 100644 index 0000000..90847ba --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/BlurredRollOverlayHost.kt @@ -0,0 +1,216 @@ +package com.pixelized.rplexicon.ui.screens.rolls + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.Stable +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.BlurredEdgeTreatment +import androidx.compose.ui.draw.blur +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.dp +import com.pixelized.rplexicon.data.model.DiceThrow +import com.pixelized.rplexicon.utilitary.extentions.lexicon +import com.pixelized.rplexicon.utilitary.extentions.modifier.clickableInterceptor +import kotlinx.coroutines.CancellableContinuation +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlin.coroutines.resume + +@Stable +interface BlurredOverlayHostState { + val isOverlayVisible: Boolean + val overlayData: BlurredOverlayData? + + suspend fun showOverlay(diceThrow: DiceThrow?): RollResult + fun dismiss() +} + +@Stable +interface BlurredOverlayData { + val diceThrow: DiceThrow + + fun setRollValue( + value: Int, + isCriticalSuccess: Boolean, + isCriticalFailure: Boolean, + ) + + fun dismiss() +} + +@Stable +sealed class RollResult { + data class Roll( + val value: Int, + val isCriticalSuccess: Boolean, + val isCriticalFailure: Boolean, + ) : RollResult() + + data object Dismissed : RollResult() + + data object Void : RollResult() +} + +@Stable +private class BlurredOverlayHostStateImpl( + rollOverlayVisibilityState: MutableState, + private val onPrepareRoll: State Unit>, + private val onShowOverlay: State<(BlurredOverlayData) -> Unit>, +) : BlurredOverlayHostState { + private val mutex = Mutex() + + private var _currentBlurredOverlayData: BlurredOverlayDataImpl? by mutableStateOf(null) + override val overlayData: BlurredOverlayData? get() = _currentBlurredOverlayData + + override var isOverlayVisible by rollOverlayVisibilityState + + override suspend fun showOverlay( + diceThrow: DiceThrow?, + ): RollResult { + return if (diceThrow == null) { + RollResult.Void + } else { + mutex.withLock { + try { + onPrepareRoll.value.invoke(diceThrow) + isOverlayVisible = true + return suspendCancellableCoroutine { continuation -> + val data = BlurredOverlayDataImpl( + continuation = continuation, + diceThrow = diceThrow + ) + _currentBlurredOverlayData = data + onShowOverlay.value.invoke(data) + } + } finally { + _currentBlurredOverlayData = null + } + } + } + } + + override fun dismiss() { + isOverlayVisible = false + _currentBlurredOverlayData?.dismiss() + } +} + +private class BlurredOverlayDataImpl( + private val continuation: CancellableContinuation, + override val diceThrow: DiceThrow, +) : BlurredOverlayData { + + private var result: RollResult = RollResult.Dismissed + + override fun setRollValue( + value: Int, + isCriticalSuccess: Boolean, + isCriticalFailure: Boolean, + ) { + if (continuation.isActive) { + result = RollResult.Roll( + value = value, + isCriticalSuccess = isCriticalSuccess, + isCriticalFailure = isCriticalFailure, + ) + } + } + + override fun dismiss() { + if (continuation.isActive) { + continuation.resume(result) + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + other as BlurredOverlayDataImpl + return continuation == other.continuation + } + + override fun hashCode(): Int { + return continuation.hashCode() + } +} + +@Composable +@Stable +fun rememberBlurredOverlayHostState( + onPrepareRoll: suspend (DiceThrow?) -> Unit, + onShowOverlay: (BlurredOverlayData) -> Unit, +): BlurredOverlayHostState { + val rollOverlayVisibilityState = rememberSaveable { mutableStateOf(false) } + + val currentOnPrepareRoll = rememberUpdatedState(newValue = onPrepareRoll) + val currentOnShowOverlay = rememberUpdatedState(newValue = onShowOverlay) + + return remember(currentOnShowOverlay, rollOverlayVisibilityState) { + BlurredOverlayHostStateImpl( + rollOverlayVisibilityState = rollOverlayVisibilityState, + onPrepareRoll = currentOnPrepareRoll, + onShowOverlay = currentOnShowOverlay, + ) + } +} + +@Composable +fun BlurredRollOverlayHost( + rollViewModel: RollOverlayViewModel, + state: BlurredOverlayHostState, + content: @Composable () -> Unit, +) { + val density = LocalDensity.current + + Surface { + val blurs = animateDpAsState( + targetValue = if (state.isOverlayVisible) 4.dp else 0.dp, + label = "RollOverlayHostBlurAnimation", + ) + Box( + modifier = Modifier.blur( + radius = blurs.value, + edgeTreatment = BlurredEdgeTreatment.Unbounded, + ), + content = { content() }, + ) + AnimatedVisibility( + visible = state.isOverlayVisible, + enter = fadeIn() + slideInVertically { with(density) { 64.dp.roundToPx() } }, + exit = fadeOut() + slideOutVertically { with(density) { 64.dp.roundToPx() } }, + content = { + Box( + modifier = Modifier + .clickableInterceptor() + .background(color = MaterialTheme.lexicon.colorScheme.rollOverlayBrush) + .fillMaxSize() + .systemBarsPadding(), + ) { + RollOverlay( + viewModel = rollViewModel, + ) + } + }, + ) + } +} \ No newline at end of file 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 27f6a83..aa4584a 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 @@ -5,16 +5,9 @@ import android.content.res.Configuration.UI_MODE_NIGHT_YES import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.ContentTransform -import androidx.compose.animation.SizeTransform import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.AnimationVector1D import androidx.compose.animation.core.animateDpAsState -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.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.isSystemInDarkTheme @@ -42,7 +35,6 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.rememberDrawerState import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState import androidx.compose.runtime.Stable import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf @@ -50,13 +42,10 @@ 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.composed import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString @@ -65,7 +54,6 @@ import androidx.compose.ui.text.style.TextOverflow 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.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp @@ -73,9 +61,7 @@ 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.data.model.DiceThrow import com.pixelized.rplexicon.isInDarkTheme -import com.pixelized.rplexicon.ui.composable.BlurredOverlayHostState import com.pixelized.rplexicon.ui.composable.CategoryHeader import com.pixelized.rplexicon.ui.composable.ModalNavigationDrawer import com.pixelized.rplexicon.ui.composable.Toolbar @@ -87,7 +73,6 @@ 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 @@ -118,7 +103,7 @@ fun RollOverlay( scope.launch { drawer.close() } }, onClose = { - overlay.hideOverlay() + overlay.dismiss() }, onDice = { viewModel.roll(uiScope = scope) @@ -466,43 +451,4 @@ private class RollOverlayPreviewProvider : PreviewParameterProvider, -) : BlurredRollOverlayHostState { - override var isOverlayVisible by rollOverlayVisibilityState - - override suspend fun prepareRoll(diceThrow: DiceThrow) { - viewModel.prepareRoll(diceThrow) - } - - override fun showOverlay() { - isOverlayVisible = true - } - - override fun hideOverlay() { - isOverlayVisible = false - } -} - -@Composable -@Stable -fun rememberBlurredRollOverlayHostState( - viewModel: RollOverlayViewModel, -): BlurredRollOverlayHostState { - val rollOverlayVisibilityState = rememberSaveable { mutableStateOf(false) } - return remember { - BlurredRollOverlayHostStateImpl( - viewModel = viewModel, - rollOverlayVisibilityState = rollOverlayVisibilityState - ) - } } \ No newline at end of file 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 dbc0c26..cbaf08a 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 @@ -62,6 +62,7 @@ class RollOverlayViewModel @Inject constructor( activeAlterationRepository: ActiveAlterationRepository, application: Application, ) : AndroidViewModel(application) { + private var _overlay: BlurredOverlayData? = null private val _diceThrow = MutableStateFlow(null) private val _alterationsOverride = MutableStateFlow>(emptyMap()) @@ -126,6 +127,10 @@ class RollOverlayViewModel @Inject constructor( private val _alterationDetailDialog = mutableStateOf(null) val alterationDetailDialog: State get() = _alterationDetailDialog + fun setRollOverlayData(data: BlurredOverlayData) { + _overlay = data + } + suspend fun prepareRoll(diceThrow: DiceThrow) { // save the dice throw. _diceThrow.value = diceThrow @@ -184,6 +189,11 @@ class RollOverlayViewModel @Inject constructor( delay(RollDiceUio.ROLL_DURATION.toLong()) // display the roll result & share with other player if (isActive) { + _overlay?.setRollValue( + value = result.throws.result.toIntOrNull() ?: 0, + isCriticalSuccess = result.throws.isCriticalSuccess ?: false, + isCriticalFailure = result.throws.isCriticalFailure ?: false, + ) _dice.value = result.dice // share the result withContext(Dispatchers.IO) {