Add Critical damage support.

This commit is contained in:
Thomas Andres Gomez 2023-09-23 10:30:56 +02:00
parent 880d19eb20
commit b6c7009697
13 changed files with 341 additions and 113 deletions

View file

@ -17,6 +17,7 @@ data class CharacterSheet(
val spell7: Counter?, val spell7: Counter?,
val spell8: Counter?, val spell8: Counter?,
val spell9: Counter?, val spell9: Counter?,
val criticalModifier: Int, // Critical Dice Multiplier
val armorClass: String, // Classe d'armure val armorClass: String, // Classe d'armure
val speed: Int, // Vitesse val speed: Int, // Vitesse
val strength: Int, // Force val strength: Int, // Force

View file

@ -1,6 +1,8 @@
package com.pixelized.rplexicon.model package com.pixelized.rplexicon.model
data class Roll( data class Roll(
val character: String,
val canUseCriticalDice: Boolean,
val title: String, val title: String,
val highlight: String? = null, val highlight: String? = null,
val dices: List<Dice>, val dices: List<Dice>,

View file

@ -1,8 +1,8 @@
package com.pixelized.rplexicon.repository.parser package com.pixelized.rplexicon.repository.parser
import com.google.api.services.sheets.v4.model.ValueRange import com.google.api.services.sheets.v4.model.ValueRange
import com.pixelized.rplexicon.repository.parser.alteration.CounterParser
import com.pixelized.rplexicon.model.CharacterSheet import com.pixelized.rplexicon.model.CharacterSheet
import com.pixelized.rplexicon.repository.parser.alteration.CounterParser
import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure
import com.pixelized.rplexicon.utilitary.extentions.local.checkSheetStructure import com.pixelized.rplexicon.utilitary.extentions.local.checkSheetStructure
import com.pixelized.rplexicon.utilitary.extentions.sheet import com.pixelized.rplexicon.utilitary.extentions.sheet
@ -54,50 +54,33 @@ class CharacterSheetParser @Inject constructor(
maxHitPoint = item.parseString(MAX_HIT_POINT) ?: "1", maxHitPoint = item.parseString(MAX_HIT_POINT) ?: "1",
lifeDice = item.parseInt(LIFE_DICE) ?: 0, lifeDice = item.parseInt(LIFE_DICE) ?: 0,
spell1 = counterParser.parseCounter( spell1 = counterParser.parseCounter(
value = item.parseString( value = item.parseString(SPELL_LEVEL_1)
SPELL_LEVEL_1
)
), ),
spell2 = counterParser.parseCounter( spell2 = counterParser.parseCounter(
value = item.parseString( value = item.parseString(SPELL_LEVEL_2)
SPELL_LEVEL_2
)
), ),
spell3 = counterParser.parseCounter( spell3 = counterParser.parseCounter(
value = item.parseString( value = item.parseString(SPELL_LEVEL_3)
SPELL_LEVEL_3
)
), ),
spell4 = counterParser.parseCounter( spell4 = counterParser.parseCounter(
value = item.parseString( value = item.parseString(SPELL_LEVEL_4)
SPELL_LEVEL_4
)
), ),
spell5 = counterParser.parseCounter( spell5 = counterParser.parseCounter(
value = item.parseString( value = item.parseString(SPELL_LEVEL_5)
SPELL_LEVEL_5
)
), ),
spell6 = counterParser.parseCounter( spell6 = counterParser.parseCounter(
value = item.parseString( value = item.parseString(SPELL_LEVEL_6)
SPELL_LEVEL_6
)
), ),
spell7 = counterParser.parseCounter( spell7 = counterParser.parseCounter(
value = item.parseString( value = item.parseString(SPELL_LEVEL_7)
SPELL_LEVEL_7
)
), ),
spell8 = counterParser.parseCounter( spell8 = counterParser.parseCounter(
value = item.parseString( value = item.parseString(SPELL_LEVEL_8)
SPELL_LEVEL_8
)
), ),
spell9 = counterParser.parseCounter( spell9 = counterParser.parseCounter(
value = item.parseString( value = item.parseString(SPELL_LEVEL_9)
SPELL_LEVEL_9
)
), ),
criticalModifier = item.parseInt(CRITICAL_MODIFIER) ?: 2,
armorClass = item.parseString(ARMOR_CLASS) ?: "10", armorClass = item.parseString(ARMOR_CLASS) ?: "10",
speed = item.parseInt(SPEED) ?: 10, speed = item.parseInt(SPEED) ?: 10,
strength = item.parseInt(STRENGTH) ?: 10, strength = item.parseInt(STRENGTH) ?: 10,
@ -157,6 +140,7 @@ class CharacterSheetParser @Inject constructor(
private const val SPELL_LEVEL_7 = "Sort de niveau 7" private const val SPELL_LEVEL_7 = "Sort de niveau 7"
private const val SPELL_LEVEL_8 = "Sort de niveau 8" private const val SPELL_LEVEL_8 = "Sort de niveau 8"
private const val SPELL_LEVEL_9 = "Sort de niveau 9" private const val SPELL_LEVEL_9 = "Sort de niveau 9"
private const val CRITICAL_MODIFIER = "Dé de critique"
private const val ARMOR_CLASS = "Classe d'armure" private const val ARMOR_CLASS = "Classe d'armure"
private const val SPEED = "Vitesse" private const val SPEED = "Vitesse"
private const val MASTERY = "Bonus de maîtrise" private const val MASTERY = "Bonus de maîtrise"
@ -208,6 +192,7 @@ class CharacterSheetParser @Inject constructor(
SPELL_LEVEL_7, SPELL_LEVEL_7,
SPELL_LEVEL_8, SPELL_LEVEL_8,
SPELL_LEVEL_9, SPELL_LEVEL_9,
CRITICAL_MODIFIER,
ARMOR_CLASS, ARMOR_CLASS,
SPEED, SPEED,
MASTERY, MASTERY,

View file

@ -212,7 +212,6 @@ private fun CharacterSheetContent(
Surface( Surface(
modifier = Modifier.padding(paddingValues = paddingValues), modifier = Modifier.padding(paddingValues = paddingValues),
) { ) {
ModalBottomSheetLayout( ModalBottomSheetLayout(
sheetState = sheetState, sheetState = sheetState,
sheetContent = { sheetContent = {

View file

@ -66,6 +66,7 @@ class AttackActionViewModel @Inject constructor(
return actionRoll( return actionRoll(
attack = action, attack = action,
throws = action?.hit, throws = action?.hit,
canUseCriticalDice = false,
) )
} }
@ -74,12 +75,14 @@ class AttackActionViewModel @Inject constructor(
return actionRoll( return actionRoll(
attack = action, attack = action,
throws = action?.damage, throws = action?.damage,
canUseCriticalDice = true,
) )
} }
private fun actionRoll( private fun actionRoll(
attack: Attack?, attack: Attack?,
throws: Throw?, throws: Throw?,
canUseCriticalDice: Boolean,
): Roll { ): Roll {
// build the title // build the title
val title = context.getString( val title = context.getString(
@ -123,6 +126,8 @@ class AttackActionViewModel @Inject constructor(
val fail = alterations.any { it.fail } val fail = alterations.any { it.fail }
// build the roll // build the roll
return Roll( return Roll(
character = character,
canUseCriticalDice = canUseCriticalDice,
title = title, title = title,
highlight = attack?.title, highlight = attack?.title,
dices = listOf( dices = listOf(

View file

@ -209,6 +209,7 @@ class SpellsActionViewModel @Inject constructor(
spell = spell?.spell, spell = spell?.spell,
titleRes = R.string.dice_roll_attack_damage_title, titleRes = R.string.dice_roll_attack_damage_title,
throws = spell?.effect, throws = spell?.effect,
canUseCriticalDice = true,
) )
} }
@ -221,11 +222,14 @@ class SpellsActionViewModel @Inject constructor(
throws: Throw?, throws: Throw?,
level: Int = 0, level: Int = 0,
levelThrow: Throw? = null, levelThrow: Throw? = null,
canUseCriticalDice: Boolean = false,
): Roll { ): Roll {
// build the title // build the title
val title = context.getString(titleRes, spell?.name) val title = context.getString(titleRes, spell?.name)
// build the roll // build the roll
return Roll( return Roll(
character = character,
canUseCriticalDice = canUseCriticalDice,
title = title, title = title,
highlight = spell?.name, highlight = spell?.name,
dices = listOf( dices = listOf(

View file

@ -298,6 +298,8 @@ class ProficiencyViewModel @Inject constructor(
val fail = alterations.any { it.fail } val fail = alterations.any { it.fail }
return Roll( return Roll(
character = character,
canUseCriticalDice = false,
title = context.getString(R.string.dice_roll_check_title, ability.uppercase()), title = context.getString(R.string.dice_roll_check_title, ability.uppercase()),
highlight = ability, highlight = ability,
dices = listOf( dices = listOf(
@ -337,6 +339,8 @@ class ProficiencyViewModel @Inject constructor(
val fail = alterations.any { it.fail } val fail = alterations.any { it.fail }
return Roll( return Roll(
character = character,
canUseCriticalDice = false,
title = context.getString( title = context.getString(
R.string.dice_roll_saving_throw_title, R.string.dice_roll_saving_throw_title,
ability.uppercase() ability.uppercase()
@ -388,6 +392,8 @@ class ProficiencyViewModel @Inject constructor(
val fail = alterations.any { it.fail } val fail = alterations.any { it.fail }
return Roll( return Roll(
character = character,
canUseCriticalDice = false,
title = context.getString(R.string.dice_roll_check_title, ability.uppercase()), title = context.getString(R.string.dice_roll_check_title, ability.uppercase()),
highlight = ability, highlight = ability,
dices = listOf( dices = listOf(

View file

@ -9,16 +9,23 @@ import javax.inject.Inject
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
data class RollResult(
val dice: RollDiceUio?,
val card: ThrowsCardUio?,
)
class ConvertRollIntoDisplayableFactory @Inject constructor() { class ConvertRollIntoDisplayableFactory @Inject constructor() {
fun roll(roll: Roll): Pair<RollDiceUio?, ThrowsCardUio?> { fun roll(
roll: Roll,
diceMultiplier: Int = 1,
): RollResult {
val mainDices = roll.dices.firstOrNull() val mainDices = roll.dices.firstOrNull()
val bonusDicesList = roll.dices.subList(fromIndex = 1, toIndex = roll.dices.size) val bonusDicesList = roll.dices.subList(fromIndex = 1, toIndex = roll.dices.size)
val allRolledValues = mutableListOf<Int>() val allRolledValues = mutableListOf<Int>()
val (rollDice, rollDetail) = mainDices?.let { dice -> val (rollDice, rollDetail) = mainDices?.let { dice ->
val (label, sum) = dice.roll() val (label, sum) = dice.roll(multiplier = diceMultiplier)
allRolledValues.add(sum) allRolledValues.add(sum)
val diceIcon = dice.toRollCardUio(result = sum) val diceIcon = dice.toRollCardUio(result = sum)
@ -31,7 +38,7 @@ class ConvertRollIntoDisplayableFactory @Inject constructor() {
val isCriticalFailure = rollDice?.isCriticalFailure ?: false val isCriticalFailure = rollDice?.isCriticalFailure ?: false
val diceBonus = bonusDicesList.map { dice -> val diceBonus = bonusDicesList.map { dice ->
val (label, sum) = dice.roll() val (label, sum) = dice.roll(multiplier = diceMultiplier)
allRolledValues.add(sum) allRolledValues.add(sum)
dice.toThrowsCardUio( dice.toThrowsCardUio(
@ -49,23 +56,25 @@ class ConvertRollIntoDisplayableFactory @Inject constructor() {
) )
} }
return rollDice to ThrowsCardUio( return RollResult(
title = roll.title, dice = rollDice,
highlight = roll.highlight, card = ThrowsCardUio(
dice = mainDices?.faces?.icon, title = roll.title,
roll = allRolledValues.toLabel(), highlight = roll.highlight,
result = when { dice = mainDices?.faces?.icon,
isCriticalSuccess -> (mainDices?.faces ?: 20).toString() roll = allRolledValues.toLabel(),
isCriticalFailure -> "1" result = when {
else -> "${allRolledValues.sum()}" isCriticalSuccess -> (mainDices?.faces ?: 20).toString()
}, isCriticalFailure -> "1"
isCriticalSuccess = isCriticalSuccess, else -> "${allRolledValues.sum()}"
isCriticalFailure = isCriticalFailure, },
details = rollDetail + diceBonus + flatBonus, isCriticalSuccess = isCriticalSuccess,
isCriticalFailure = isCriticalFailure,
details = rollDetail + diceBonus + flatBonus,
),
) )
} }
private fun Roll.Dice.toRollCardUio(result: Int) = RollDiceUio( private fun Roll.Dice.toRollCardUio(result: Int) = RollDiceUio(
icon = faces.icon, icon = faces.icon,
isCriticalSuccess = count == 1 && faces == 20 && result == faces, isCriticalSuccess = count == 1 && faces == 20 && result == faces,
@ -85,33 +94,33 @@ class ConvertRollIntoDisplayableFactory @Inject constructor() {
result = "$result", result = "$result",
) )
private data class RollResult( private data class DiceRollResult(
val label: String, val label: String,
val sum: Int, val sum: Int,
) )
private fun Roll.Dice.roll(): RollResult { private fun Roll.Dice.roll(multiplier: Int): DiceRollResult {
return when { return when {
advantage && !disadvantage -> { advantage && !disadvantage -> {
val roll = List(count) { random() to random() } val roll = List(count * multiplier) { random() to random() }
RollResult( DiceRollResult(
label = roll.joinToString(" + ") { "${it.first}~${it.second}" }, label = roll.joinToString(" + ") { "${it.first}~${it.second}" },
sum = roll.sumOf { max(it.first, it.second) }, sum = roll.sumOf { max(it.first, it.second) },
) )
} }
disadvantage && !advantage -> { disadvantage && !advantage -> {
val roll = List(count) { random() to random() } val roll = List(count * multiplier) { random() to random() }
RollResult( DiceRollResult(
label = roll.joinToString(" + ") { "${it.first}~${it.second}" }, label = roll.joinToString(" + ") { "${it.first}~${it.second}" },
sum = roll.sumOf { min(it.first, it.second) }, sum = roll.sumOf { min(it.first, it.second) },
) )
} }
else -> { else -> {
val roll = List(count) { random() } val roll = List(count * multiplier) { random() }
RollResult( DiceRollResult(
label = roll.toLabel(), label = roll.toLabel(),
sum = roll.sum(), sum = roll.sum(),
) )

View file

@ -2,6 +2,7 @@ package com.pixelized.rplexicon.ui.screens.rolls
import android.content.res.Configuration.UI_MODE_NIGHT_NO import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.SizeTransform import androidx.compose.animation.SizeTransform
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
@ -11,14 +12,26 @@ import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith import androidx.compose.animation.togetherWith
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Close import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.Menu
import androidx.compose.material3.DrawerState
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
@ -26,124 +39,194 @@ import androidx.compose.runtime.State
import androidx.compose.runtime.getValue 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.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue 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.graphics.Color
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
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.PreviewParameterProvider
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel 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.R import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.model.Roll import com.pixelized.rplexicon.model.Roll
import com.pixelized.rplexicon.ui.composable.BlurredOverlayHostState import com.pixelized.rplexicon.ui.composable.BlurredOverlayHostState
import com.pixelized.rplexicon.ui.screens.rolls.composable.RollAlteration
import com.pixelized.rplexicon.ui.screens.rolls.composable.RollAlterationUio
import com.pixelized.rplexicon.ui.screens.rolls.composable.RollDice import com.pixelized.rplexicon.ui.screens.rolls.composable.RollDice
import com.pixelized.rplexicon.ui.screens.rolls.composable.RollDiceUio 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.theme.LexiconTheme import com.pixelized.rplexicon.ui.theme.LexiconTheme
import kotlinx.coroutines.launch
@Composable @Composable
fun RollOverlay( fun RollOverlay(
viewModel: RollOverlayViewModel = hiltViewModel(), viewModel: RollOverlayViewModel = hiltViewModel(),
) { ) {
val overlay = LocalRollOverlay.current val overlay = LocalRollOverlay.current
val scope = rememberCoroutineScope()
val drawer = rememberDrawerState(initialValue = DrawerValue.Closed)
RollOverlayContent( RollOverlayContent(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
drawer = drawer,
dice = viewModel.dice, dice = viewModel.dice,
card = viewModel.card, card = viewModel.card,
alterations = viewModel.alterations,
showDetail = viewModel.showDetail, showDetail = viewModel.showDetail,
onMenu = {
scope.launch { drawer.open() }
},
onClose = {
overlay.hideOverlay()
},
onDice = { onDice = {
viewModel.roll() viewModel.roll()
}, },
onCard = { onCard = {
viewModel.toggleDetail() viewModel.toggleDetail()
}, },
onClose = { onAlteration = {
overlay.hideOverlay() viewModel.onAlteration(it)
} },
) )
BackHandler(enabled = drawer.isOpen) {
scope.launch { drawer.close() }
}
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun RollOverlayContent( private fun RollOverlayContent(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
drawer: DrawerState,
dice: State<RollDiceUio>, dice: State<RollDiceUio>,
card: State<ThrowsCardUio?>, card: State<ThrowsCardUio?>,
alterations: State<List<RollAlterationUio>>,
showDetail: State<Boolean>, showDetail: State<Boolean>,
onMenu: () -> Unit,
onClose: () -> Unit,
onDice: () -> Unit, onDice: () -> Unit,
onCard: () -> Unit, onCard: () -> Unit,
onClose: () -> Unit, onAlteration: (id: String) -> Unit,
) { ) {
val density = LocalDensity.current val density = LocalDensity.current
Box( ModalNavigationDrawer(
modifier = modifier, modifier = modifier,
) { drawerState = drawer,
RollDice( drawerContent = {
modifier = Modifier Surface {
.align(alignment = Alignment.Center) LazyColumn(
.padding(horizontal = 16.dp),
dice = dice,
onDice = onDice,
)
AnimatedContent(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomCenter)
.padding(all = 16.dp),
targetState = card.value,
transitionSpec = {
val enter = fadeIn() + slideInVertically { with(density) { 64.dp.roundToPx() } }
val exit = fadeOut() + slideOutVertically { with(density) { -64.dp.roundToPx() } }
val transform = SizeTransform(clip = false)
enter togetherWith exit using transform
},
label = "RollOverlayDisplay",
) {
when (it) {
null -> Box(
modifier = Modifier.fillMaxWidth(),
)
else -> ThrowsCard(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxHeight()
.clickable(onClick = onCard), .fillMaxWidth(4f / 5f),
throws = it, ) {
showDetail = showDetail, items(items = alterations.value) {
) RollAlteration(
modifier = Modifier.fillMaxWidth(),
alteration = it,
onClick = onAlteration,
)
}
}
} }
} },
content = {
Scaffold(
containerColor = Color.Transparent,
contentWindowInsets = NO_WINDOW_INSETS,
topBar = {
TopAppBar(
colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent),
title = { },
navigationIcon = {
IconButton(onClick = onMenu) {
Icon(
imageVector = Icons.Outlined.Menu,
contentDescription = null,
)
}
},
actions = {
IconButton(onClick = onClose) {
Icon(
imageVector = Icons.Outlined.Close,
contentDescription = null,
)
}
},
)
},
content = { paddingValues ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
) {
RollDice(
modifier = Modifier
.align(alignment = Alignment.Center)
.padding(horizontal = 16.dp),
dice = dice,
onDice = onDice,
)
IconButton( AnimatedContent(
modifier = Modifier modifier = Modifier
.align(alignment = Alignment.TopEnd) .fillMaxWidth()
.padding( .align(Alignment.BottomCenter)
top = 6.dp, .padding(all = 16.dp),
end = 4.dp targetState = card.value,
), transitionSpec = {
onClick = onClose val enter =
) { fadeIn() + slideInVertically { with(density) { 64.dp.roundToPx() } }
Icon( val exit =
imageVector = Icons.Outlined.Close, fadeOut() + slideOutVertically { with(density) { -64.dp.roundToPx() } }
contentDescription = null, val transform = SizeTransform(clip = false)
enter togetherWith exit using transform
},
label = "RollOverlayDisplay",
) {
when (it) {
null -> Box(
modifier = Modifier.fillMaxWidth(),
)
else -> ThrowsCard(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onCard),
throws = it,
showDetail = showDetail,
)
}
}
}
}
) )
} },
} )
} }
@Composable @Composable
@Preview(uiMode = UI_MODE_NIGHT_NO) @Preview(uiMode = UI_MODE_NIGHT_NO)
@Preview(uiMode = UI_MODE_NIGHT_YES) @Preview(uiMode = UI_MODE_NIGHT_YES)
private fun RollOverlayPreview() { private fun RollOverlayPreview(
@PreviewParameter(RollOverlayPreviewProvider::class) preview: DrawerValue
) {
LexiconTheme { LexiconTheme {
Surface { Surface {
RollOverlayContent( RollOverlayContent(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
drawer = rememberDrawerState(initialValue = preview),
dice = remember { dice = remember {
mutableStateOf( mutableStateOf(
RollDiceUio( RollDiceUio(
@ -183,17 +266,30 @@ private fun RollOverlayPreview() {
) )
) )
}, },
alterations = remember {
mutableStateOf(
listOf(
RollAlterationUio(label = "Critique", checked = false),
)
)
},
showDetail = remember { showDetail = remember {
mutableStateOf(true) mutableStateOf(true)
}, },
onMenu = { },
onClose = { },
onDice = { }, onDice = { },
onCard = { }, onCard = { },
onClose = { }, onAlteration = { },
) )
} }
} }
} }
private class RollOverlayPreviewProvider : PreviewParameterProvider<DrawerValue> {
override val values: Sequence<DrawerValue> = sequenceOf(DrawerValue.Closed, DrawerValue.Open)
}
@Stable @Stable
interface BlurredRollOverlayHostState : BlurredOverlayHostState { interface BlurredRollOverlayHostState : BlurredOverlayHostState {
fun prepareRoll(roll: Roll) fun prepareRoll(roll: Roll)

View file

@ -1,12 +1,21 @@
package com.pixelized.rplexicon.ui.screens.rolls package com.pixelized.rplexicon.ui.screens.rolls
import android.app.Application
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.model.Alteration
import com.pixelized.rplexicon.model.CharacterSheet
import com.pixelized.rplexicon.model.Roll import com.pixelized.rplexicon.model.Roll
import com.pixelized.rplexicon.repository.data.AlterationRepository
import com.pixelized.rplexicon.repository.data.CharacterSheetRepository
import com.pixelized.rplexicon.ui.screens.rolls.composable.RollAlterationUio
import com.pixelized.rplexicon.ui.screens.rolls.composable.RollDiceUio import com.pixelized.rplexicon.ui.screens.rolls.composable.RollDiceUio
import com.pixelized.rplexicon.ui.screens.rolls.composable.ThrowsCardUio import com.pixelized.rplexicon.ui.screens.rolls.composable.ThrowsCardUio
import com.pixelized.rplexicon.utilitary.extentions.context
import com.pixelized.rplexicon.utilitary.extentions.icon import com.pixelized.rplexicon.utilitary.extentions.icon
import com.pixelized.rplexicon.utilitary.extentions.switch import com.pixelized.rplexicon.utilitary.extentions.switch
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
@ -17,11 +26,18 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class RollOverlayViewModel @Inject constructor( class RollOverlayViewModel @Inject constructor(
application: Application,
private val characterSheetRepository: CharacterSheetRepository,
private val factory: ConvertRollIntoDisplayableFactory, private val factory: ConvertRollIntoDisplayableFactory,
) : ViewModel() { ) : AndroidViewModel(application) {
private val criticalLabel = context.getString(R.string.dice_roll_critical_label)
private var rollJob: Job? = null private var rollJob: Job? = null
private lateinit var roll: Roll private lateinit var roll: Roll
private var character: CharacterSheet? = null
private val _alterations = mutableStateOf<List<RollAlterationUio>>(emptyList())
val alterations: State<List<RollAlterationUio>> get() = _alterations
private val _dice = mutableStateOf(RollDiceUio()) private val _dice = mutableStateOf(RollDiceUio())
val dice: State<RollDiceUio> get() = _dice val dice: State<RollDiceUio> get() = _dice
@ -34,14 +50,38 @@ class RollOverlayViewModel @Inject constructor(
fun prepareRoll(roll: Roll) { fun prepareRoll(roll: Roll) {
this.roll = roll this.roll = roll
this.character = characterSheetRepository.find(name = roll.character)
_dice.value = RollDiceUio(icon = roll.dices.maxOf { it.faces }.icon) _dice.value = RollDiceUio(icon = roll.dices.maxOf { it.faces }.icon)
_card.value = null _card.value = null
_alterations.value = if (roll.canUseCriticalDice) {
listOf(RollAlterationUio(label = criticalLabel, checked = false))
} else {
emptyList()
}
}
fun onAlteration(id: String) {
_alterations.value = _alterations.value.toMutableList().also { alterations ->
val index = alterations.indexOfFirst { it.label == id }
val alteration = alterations[index].let { it.copy(checked = it.checked.not()) }
alterations.removeAt(index)
alterations.add(index, alteration)
}
} }
fun roll() { fun roll() {
rollJob?.cancel() rollJob?.cancel()
rollJob = viewModelScope.launch { rollJob = viewModelScope.launch {
val (dice, card) = factory.roll(roll = roll) val isCritical = _alterations.value.firstOrNull { it.label == criticalLabel }?.checked ?: false
val (dice, card) = factory.roll(
roll = roll,
diceMultiplier = when {
isCritical -> character?.criticalModifier ?: 2
else -> 1
},
)
// Start the roll animation. // Start the roll animation.
_dice.value = _dice.value.copy( _dice.value = _dice.value.copy(
animationSpec = RollDiceUio.TWEEN, animationSpec = RollDiceUio.TWEEN,

View file

@ -0,0 +1,79 @@
package com.pixelized.rplexicon.ui.screens.rolls.composable
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.pixelized.rplexicon.ui.theme.LexiconTheme
@Stable
data class RollAlterationUio(
val label: String,
val checked: Boolean
)
@Composable
fun RollAlteration(
modifier: Modifier = Modifier,
alteration: RollAlterationUio,
onClick: (id: String) -> Unit,
) {
Row(
modifier = modifier
.clickable { onClick(alteration.label) }
.padding(horizontal = 16.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(
style = MaterialTheme.typography.bodyMedium,
text = alteration.label,
)
Switch(
enabled = true,
checked = alteration.checked,
onCheckedChange = { onClick(alteration.label) },
)
}
}
@Composable
@Preview(uiMode = UI_MODE_NIGHT_NO)
@Preview(uiMode = UI_MODE_NIGHT_YES)
private fun RollAlterationPreview(
@PreviewParameter(RollAlterationPreviewProvider::class) preview: RollAlterationUio,
) {
LexiconTheme {
Surface {
RollAlteration(
modifier = Modifier.fillMaxWidth(),
alteration = preview,
onClick = { },
)
}
}
}
private class RollAlterationPreviewProvider : PreviewParameterProvider<RollAlterationUio> {
override val values: Sequence<RollAlterationUio> = sequenceOf(
RollAlterationUio(label = "Critique", checked = false),
RollAlterationUio(label = "Rage", checked = true),
RollAlterationUio(label = "Bénédiction", checked = false),
)
}

View file

@ -127,6 +127,7 @@
<string name="dice_roll_attack_damage_title">Jet de dommage : \"%1$s\"</string> <string name="dice_roll_attack_damage_title">Jet de dommage : \"%1$s\"</string>
<string name="dice_roll_spell_hit_title">Jet de sort : \"%1$s"</string> <string name="dice_roll_spell_hit_title">Jet de sort : \"%1$s"</string>
<string name="dice_roll_spell_damage_title">Jet de dommage : \"%1$s\"</string> <string name="dice_roll_spell_damage_title">Jet de dommage : \"%1$s\"</string>
<string name="dice_roll_critical_label">Critique</string>
<string name="dice_roll_saving_throw_title">JET DE SAUVEGARDE : %1$s</string> <string name="dice_roll_saving_throw_title">JET DE SAUVEGARDE : %1$s</string>
<string name="dice_roll_saving_throw_detail">Sauvegarde de \"%1$s\"</string> <string name="dice_roll_saving_throw_detail">Sauvegarde de \"%1$s\"</string>

View file

@ -127,6 +127,7 @@
<string name="dice_roll_attack_damage_title">%1$s DAMAGE</string> <string name="dice_roll_attack_damage_title">%1$s DAMAGE</string>
<string name="dice_roll_spell_hit_title">%1$s HIT</string> <string name="dice_roll_spell_hit_title">%1$s HIT</string>
<string name="dice_roll_spell_damage_title">%1$s DAMAGE</string> <string name="dice_roll_spell_damage_title">%1$s DAMAGE</string>
<string name="dice_roll_critical_label">Critical</string>
<string name="dice_roll_saving_throw_title">%1$s SAVING THROW</string> <string name="dice_roll_saving_throw_title">%1$s SAVING THROW</string>
<string name="dice_roll_saving_throw_detail">%1$s save</string> <string name="dice_roll_saving_throw_detail">%1$s save</string>