Add the "know what you roll" feature, allow automation after a roll.

This commit is contained in:
Andres Gomez, Thomas (ITDV RL) 2024-08-20 14:44:52 +02:00
parent c4df543b3d
commit ac71765c44
14 changed files with 312 additions and 224 deletions

View file

@ -2,6 +2,7 @@ package com.pixelized.rplexicon
import android.app.Activity import android.app.Activity
import android.os.Bundle import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
@ -30,13 +31,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat 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.composable.error.HandleFetchError
import com.pixelized.rplexicon.ui.navigation.ScreenNavHost import com.pixelized.rplexicon.ui.navigation.ScreenNavHost
import com.pixelized.rplexicon.ui.screens.rolls.BlurredRollOverlayHostState import com.pixelized.rplexicon.ui.screens.rolls.BlurredOverlayHostState
import com.pixelized.rplexicon.ui.screens.rolls.RollOverlay import com.pixelized.rplexicon.ui.screens.rolls.BlurredRollOverlayHost
import com.pixelized.rplexicon.ui.screens.rolls.RollOverlayViewModel 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.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.extentions.modifier.doubleBorder import com.pixelized.rplexicon.utilitary.extentions.modifier.doubleBorder
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@ -49,7 +49,7 @@ val LocalActivity = compositionLocalOf<Activity> {
val LocalSnack = compositionLocalOf<SnackbarHostState> { val LocalSnack = compositionLocalOf<SnackbarHostState> {
error("SnackbarHostState not available") error("SnackbarHostState not available")
} }
val LocalRollOverlay = compositionLocalOf<BlurredRollOverlayHostState> { val LocalRollOverlay = compositionLocalOf<BlurredOverlayHostState> {
error("LocalRollOverlay not yet ready") error("LocalRollOverlay not yet ready")
} }
@ -73,8 +73,15 @@ class MainActivity : ComponentActivity() {
setContent { setContent {
val snack = remember { SnackbarHostState() } val snack = remember { SnackbarHostState() }
val overlay = rememberBlurredRollOverlayHostState( val overlay = rememberBlurredOverlayHostState(
viewModel = rollViewModel, onPrepareRoll = { dice ->
dice?.let {
rollViewModel.prepareRoll(diceThrow = it)
}
},
onShowOverlay = { data ->
rollViewModel.setRollOverlayData(data)
}
) )
CompositionLocalProvider( CompositionLocalProvider(
@ -94,13 +101,9 @@ class MainActivity : ComponentActivity() {
.padding(paddingValues = padding), .padding(paddingValues = padding),
color = MaterialTheme.colorScheme.background color = MaterialTheme.colorScheme.background
) { ) {
BlurredOverlayHost( BlurredRollOverlayHost(
rollOverlayState = overlay, rollViewModel = rollViewModel,
overlay = { state = overlay,
RollOverlay(
viewModel = rollViewModel,
)
},
content = { content = {
ScreenNavHost() ScreenNavHost()
}, },
@ -150,7 +153,7 @@ class MainActivity : ComponentActivity() {
BackHandler( BackHandler(
enabled = overlay.isOverlayVisible, enabled = overlay.isOverlayVisible,
onBack = { overlay.hideOverlay() }, onBack = { overlay.dismiss() },
) )
HandleFetchError( HandleFetchError(

View file

@ -11,7 +11,7 @@ data class Item(
val context: String?, val context: String?,
val isContainer: Boolean, val isContainer: Boolean,
val effect: Throw?, val effect: Throw?,
val usable: Boolean, val consumable: Boolean,
val icon: Uri?, val icon: Uri?,
) { ) {
val fullName: String = prefix?.let { "$it${name}" } ?: name val fullName: String = prefix?.let { "$it${name}" } ?: name

View file

@ -31,7 +31,7 @@ class ItemLexiconParser @Inject constructor(
type = row.parse(column = TYPE), type = row.parse(column = TYPE),
context = row.parse(column = CONTEXT), context = row.parse(column = CONTEXT),
isContainer = row.parseBool(column = CONTAINER) ?: false, 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)), effect = throwParser.parse(value = row.parse(column = EFFECT)),
icon = row.parseUri(column = ICON), icon = row.parseUri(column = ICON),
) )
@ -51,11 +51,11 @@ class ItemLexiconParser @Inject constructor(
private val TYPE = column("Type") private val TYPE = column("Type")
private val CONTEXT = column("Contexte") private val CONTEXT = column("Contexte")
private val CONTAINER = column("Contenant") private val CONTAINER = column("Contenant")
private val USABLE = column("Utilisable") private val CONSUMABLE = column("Consommable")
private val EFFECT = column("Effet") private val EFFECT = column("Effet")
private val ICON = column("Icone") private val ICON = column("Icone")
private val COLUMNS = private val COLUMNS =
listOf(ID, PREFIX, NAME, TYPE, CONTEXT, CONTAINER, USABLE, EFFECT, ICON) listOf(ID, PREFIX, NAME, TYPE, CONTEXT, CONTAINER, CONSUMABLE, EFFECT, ICON)
} }
} }

View file

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

View file

@ -59,6 +59,7 @@ import com.pixelized.rplexicon.NO_WINDOW_INSETS
import com.pixelized.rplexicon.R import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.ui.composable.KeepOnScreen import com.pixelized.rplexicon.ui.composable.KeepOnScreen
import com.pixelized.rplexicon.ui.composable.Loader 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.Toolbar
import com.pixelized.rplexicon.ui.composable.edit.HandleHitPointEditDialog import com.pixelized.rplexicon.ui.composable.edit.HandleHitPointEditDialog
import com.pixelized.rplexicon.ui.composable.edit.HandleSkillEditDialog 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.AlterationPage
import com.pixelized.rplexicon.ui.screens.character.pages.alteration.AlterationPagePreview 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.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.InventoryPage2
import com.pixelized.rplexicon.ui.screens.character.pages.inventory.InventoryPreview 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.ProficiencyPage
import com.pixelized.rplexicon.ui.screens.character.pages.proficiency.ProficiencyPreview import com.pixelized.rplexicon.ui.screens.character.pages.proficiency.ProficiencyPreview
import com.pixelized.rplexicon.ui.screens.character.pages.proficiency.ProficiencyViewModel import com.pixelized.rplexicon.ui.screens.character.pages.proficiency.ProficiencyViewModel
@ -149,8 +150,7 @@ fun CharacterSheetScreen(
}, },
onInitiative = { onInitiative = {
scope.launch { scope.launch {
overlay.prepareRoll(diceThrow = headerViewModel.initiativeRoll()) overlay.showOverlay(diceThrow = headerViewModel.initiativeRoll())
overlay.showOverlay()
} }
}, },
onHitPoint = headerViewModel::toggleHitPointDialog, onHitPoint = headerViewModel::toggleHitPointDialog,
@ -159,8 +159,16 @@ fun CharacterSheetScreen(
}, },
onDeathRoll = { onDeathRoll = {
scope.launch { scope.launch {
overlay.prepareRoll(diceThrow = headerViewModel.onDeathThrow()) val dices = headerViewModel.onDeathThrow()
overlay.showOverlay() 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, onDeathSuccess = headerViewModel::onDeathSuccess,
@ -203,13 +211,12 @@ fun CharacterSheetScreen(
onLevel = { spell, level -> onLevel = { spell, level ->
scope.launch { scope.launch {
sheetState.hide() sheetState.hide()
overlay.prepareRoll( overlay.showOverlay(
diceThrow = spellsViewModel.onCastSpell( diceThrow = spellsViewModel.onCastSpell(
spell, spell,
level level
) )
) )
overlay.showOverlay()
} }
}, },
) )
@ -237,7 +244,7 @@ fun CharacterSheetScreen(
} }
BackHandler(enabled = overlay.isOverlayVisible) { BackHandler(enabled = overlay.isOverlayVisible) {
overlay.hideOverlay() overlay.dismiss()
} }
KeepOnScreen() KeepOnScreen()

View file

@ -63,16 +63,14 @@ fun ActionPage(
onAttackHit = { id -> onAttackHit = { id ->
attacksViewModel.onHitRoll(id)?.let { attacksViewModel.onHitRoll(id)?.let {
scope.launch { scope.launch {
overlay.prepareRoll(diceThrow = it) overlay.showOverlay(diceThrow = it)
overlay.showOverlay()
} }
} }
}, },
onAttackDamage = { id -> onAttackDamage = { id ->
attacksViewModel.onDamageRoll(id)?.let { attacksViewModel.onDamageRoll(id)?.let {
scope.launch { scope.launch {
overlay.prepareRoll(diceThrow = it) overlay.showOverlay(diceThrow = it)
overlay.showOverlay()
} }
} }
}, },
@ -81,8 +79,7 @@ fun ActionPage(
}, },
onSkillThrow = { onSkillThrow = {
scope.launch { scope.launch {
overlay.prepareRoll(diceThrow = skillViewModel.onSkillRoll(it.label)) overlay.showOverlay(diceThrow = skillViewModel.onSkillRoll(it.label))
overlay.showOverlay()
} }
}, },
onSkillInfo = { onSkillInfo = {
@ -100,14 +97,12 @@ fun ActionPage(
}, },
onSpellHit = { id -> onSpellHit = { id ->
scope.launch { scope.launch {
overlay.prepareRoll(diceThrow = spellsViewModel.onSpellHitRoll(id)) overlay.showOverlay(diceThrow = spellsViewModel.onSpellHitRoll(id))
overlay.showOverlay()
} }
}, },
onSpellDamage = { id -> onSpellDamage = { id ->
scope.launch { scope.launch {
overlay.prepareRoll(diceThrow = spellsViewModel.onSpellDamageRoll(id)) overlay.showOverlay(diceThrow = spellsViewModel.onSpellDamageRoll(id))
overlay.showOverlay()
} }
}, },
onCast = { onCast = {
@ -116,8 +111,7 @@ fun ActionPage(
scope.launch { sheetState.show() } scope.launch { sheetState.show() }
} else { } else {
scope.launch { scope.launch {
overlay.prepareRoll(diceThrow = spellsViewModel.onCastSpell(it)) overlay.showOverlay(diceThrow = spellsViewModel.onCastSpell(it))
overlay.showOverlay()
} }
} }
}, },

View file

@ -113,19 +113,25 @@ class HeaderViewModel @Inject constructor(
return DiceThrow.DeathSavingThrow(character = character) return DiceThrow.DeathSavingThrow(character = character)
} }
fun onDeathSuccess() { fun onDeathSuccess(
critical: Boolean = false,
) {
val token = if (critical) 2 else 1
firebaseRepository.setCharacterDeathCounter( firebaseRepository.setCharacterDeathCounter(
character = character, character = character,
success = ((fireData.value?.deathSuccess ?: 0) + 1) % 4, success = ((fireData.value?.deathSuccess ?: 0) + token) % 4,
failure = fireData.value?.deathFailure ?: 0, failure = fireData.value?.deathFailure ?: 0,
) )
} }
fun onDeathFailure() { fun onDeathFailure(
critical: Boolean = false,
) {
val token = if (critical) 2 else 1
firebaseRepository.setCharacterDeathCounter( firebaseRepository.setCharacterDeathCounter(
character = character, character = character,
success = fireData.value?.deathSuccess ?: 0, success = fireData.value?.deathSuccess ?: 0,
failure = ((fireData.value?.deathFailure ?: 0) + 1) % 4, failure = ((fireData.value?.deathFailure ?: 0) + token) % 4,
) )
} }

View file

@ -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_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.ItemListDialog
import com.pixelized.rplexicon.ui.screens.character.pages.inventory.item_list.ItemListViewModel 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 com.pixelized.rplexicon.ui.theme.LexiconTheme
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -107,11 +108,14 @@ fun InventoryPage2(
}, },
onItemUse = { id, quantity -> onItemUse = { id, quantity ->
// try to roll the dices. // try to roll the dices.
val diceThrow = inventoryViewModel.useItem(itemId = id) scope.launch {
if (diceThrow != null) { val (item, dices) = inventoryViewModel.useItem(itemId = id)
scope.launch { val result = overlay.showOverlay(diceThrow = dices)
overlay.prepareRoll(diceThrow = diceThrow) if (result is RollResult.Roll && item?.consumable == true) {
overlay.showOverlay() inventoryViewModel.setItemQuantity(
itemId = id,
quantity = quantity - 1
)
} }
} }
}, },
@ -141,10 +145,14 @@ fun InventoryPage2(
// hide the detail dialog // hide the detail dialog
itemDetailViewModel.hide() itemDetailViewModel.hide()
// try to roll the dices. // try to roll the dices.
inventoryViewModel.useItem(itemId = detail.id)?.let { diceThrow -> scope.launch {
scope.launch { val (item, dices) = inventoryViewModel.useItem(itemId = detail.id)
overlay.prepareRoll(diceThrow = diceThrow) val result = overlay.showOverlay(diceThrow = dices)
overlay.showOverlay() if (result is RollResult.Roll && item?.consumable == true) {
inventoryViewModel.setItemQuantity(
itemId = detail.id,
quantity = detail.quantity?.minus(1) ?: 0
)
} }
} }
} }

View file

@ -9,6 +9,7 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.pixelized.rplexicon.data.model.DiceThrow 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.CharacterSheetRepository
import com.pixelized.rplexicon.data.repository.character.ItemsRepository import com.pixelized.rplexicon.data.repository.character.ItemsRepository
import com.pixelized.rplexicon.data.repository.firebase.inventory.InventoryFireRepository import com.pixelized.rplexicon.data.repository.firebase.inventory.InventoryFireRepository
@ -329,16 +330,18 @@ class InventoryViewModel @Inject constructor(
fun useItem( fun useItem(
itemId: String, itemId: String,
): DiceThrow? { ): Pair<Item?, DiceThrow?> {
val item = itemRepository.find(id = itemId) val item = itemRepository.find(id = itemId)
return if (item?.effect != null) { return if (item == null) {
DiceThrow.Object( null to null
character = character,
itemId = item.id,
itemName = item.name,
)
} else { } else {
null item to item.effect?.let {
DiceThrow.Object(
character = character,
itemId = item.id,
itemName = item.name,
)
}
} }
} }
} }

View file

@ -112,8 +112,7 @@ fun InventoryPageOLD(
// detailViewModel.hide(detail) // detailViewModel.hide(detail)
viewModel.onUse(itemId = detail.id)?.let { diceThrow -> viewModel.onUse(itemId = detail.id)?.let { diceThrow ->
scope.launch { scope.launch {
overlay.prepareRoll(diceThrow = diceThrow) overlay.showOverlay(diceThrow = diceThrow)
overlay.showOverlay()
} }
} }
} }

View file

@ -83,14 +83,12 @@ fun ProficiencyPage(
passives = viewModel.skills, passives = viewModel.skills,
onStats = { stat -> onStats = { stat ->
scope.launch { scope.launch {
overlay.prepareRoll(diceThrow = viewModel.statRoll(stat.id)) overlay.showOverlay(diceThrow = viewModel.statRoll(stat.id))
overlay.showOverlay()
} }
}, },
onProficiencies = { proficiency -> onProficiencies = { proficiency ->
scope.launch { scope.launch {
overlay.prepareRoll(diceThrow = viewModel.proficiencyRoll(proficiency.id)) overlay.showOverlay(diceThrow = viewModel.proficiencyRoll(proficiency.id))
overlay.showOverlay()
} }
}, },
onSkillCount = { onSkillCount = {
@ -98,8 +96,7 @@ fun ProficiencyPage(
}, },
onSkillThrow = { onSkillThrow = {
scope.launch { scope.launch {
overlay.prepareRoll(diceThrow = viewModel.onSkillRoll(it.label)) overlay.showOverlay(diceThrow = viewModel.onSkillRoll(it.label))
overlay.showOverlay()
} }
}, },
onSkillInfo = { onSkillInfo = {

View file

@ -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<Boolean>,
private val onPrepareRoll: State<suspend (DiceThrow?) -> 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<RollResult>,
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,
)
}
},
)
}
}

View file

@ -5,16 +5,9 @@ import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility 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.Animatable
import androidx.compose.animation.core.AnimationVector1D import androidx.compose.animation.core.AnimationVector1D
import androidx.compose.animation.core.animateDpAsState 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.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
@ -42,7 +35,6 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberDrawerState import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
@ -50,13 +42,10 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.composed import androidx.compose.ui.composed
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString 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.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp 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.LocalRollOverlay
import com.pixelized.rplexicon.NO_WINDOW_INSETS import com.pixelized.rplexicon.NO_WINDOW_INSETS
import com.pixelized.rplexicon.R import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.data.model.DiceThrow
import com.pixelized.rplexicon.isInDarkTheme 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.CategoryHeader
import com.pixelized.rplexicon.ui.composable.ModalNavigationDrawer import com.pixelized.rplexicon.ui.composable.ModalNavigationDrawer
import com.pixelized.rplexicon.ui.composable.Toolbar 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.ThrowsCard
import com.pixelized.rplexicon.ui.screens.rolls.composable.ThrowsCardUio 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.rememberSlideInOutAnimation
import com.pixelized.rplexicon.ui.screens.rolls.composable.slideInOutAnimation
import com.pixelized.rplexicon.ui.screens.rolls.preview.rememberRollAlterations import com.pixelized.rplexicon.ui.screens.rolls.preview.rememberRollAlterations
import com.pixelized.rplexicon.ui.theme.LexiconTheme import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.extentions.lexicon import com.pixelized.rplexicon.utilitary.extentions.lexicon
@ -118,7 +103,7 @@ fun RollOverlay(
scope.launch { drawer.close() } scope.launch { drawer.close() }
}, },
onClose = { onClose = {
overlay.hideOverlay() overlay.dismiss()
}, },
onDice = { onDice = {
viewModel.roll(uiScope = scope) viewModel.roll(uiScope = scope)
@ -466,43 +451,4 @@ private class RollOverlayPreviewProvider : PreviewParameterProvider<RollOverlayP
card = mutableStateOf(null), card = mutableStateOf(null),
), ),
) )
}
@Stable
interface BlurredRollOverlayHostState : BlurredOverlayHostState {
suspend fun prepareRoll(diceThrow: DiceThrow)
}
@Stable
private class BlurredRollOverlayHostStateImpl(
private val viewModel: RollOverlayViewModel,
rollOverlayVisibilityState: MutableState<Boolean>,
) : 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
)
}
} }

View file

@ -62,6 +62,7 @@ class RollOverlayViewModel @Inject constructor(
activeAlterationRepository: ActiveAlterationRepository, activeAlterationRepository: ActiveAlterationRepository,
application: Application, application: Application,
) : AndroidViewModel(application) { ) : AndroidViewModel(application) {
private var _overlay: BlurredOverlayData? = null
private val _diceThrow = MutableStateFlow<DiceThrow?>(null) private val _diceThrow = MutableStateFlow<DiceThrow?>(null)
private val _alterationsOverride = MutableStateFlow<Map<String, Boolean>>(emptyMap()) private val _alterationsOverride = MutableStateFlow<Map<String, Boolean>>(emptyMap())
@ -126,6 +127,10 @@ class RollOverlayViewModel @Inject constructor(
private val _alterationDetailDialog = mutableStateOf<AlterationDialogDetailUio?>(null) private val _alterationDetailDialog = mutableStateOf<AlterationDialogDetailUio?>(null)
val alterationDetailDialog: State<AlterationDialogDetailUio?> get() = _alterationDetailDialog val alterationDetailDialog: State<AlterationDialogDetailUio?> get() = _alterationDetailDialog
fun setRollOverlayData(data: BlurredOverlayData) {
_overlay = data
}
suspend fun prepareRoll(diceThrow: DiceThrow) { suspend fun prepareRoll(diceThrow: DiceThrow) {
// save the dice throw. // save the dice throw.
_diceThrow.value = diceThrow _diceThrow.value = diceThrow
@ -184,6 +189,11 @@ class RollOverlayViewModel @Inject constructor(
delay(RollDiceUio.ROLL_DURATION.toLong()) delay(RollDiceUio.ROLL_DURATION.toLong())
// display the roll result & share with other player // display the roll result & share with other player
if (isActive) { if (isActive) {
_overlay?.setRollValue(
value = result.throws.result.toIntOrNull() ?: 0,
isCriticalSuccess = result.throws.isCriticalSuccess ?: false,
isCriticalFailure = result.throws.isCriticalFailure ?: false,
)
_dice.value = result.dice _dice.value = result.dice
// share the result // share the result
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {