Link the CharacterSheet model to the UI.

This commit is contained in:
Thomas Andres Gomez 2024-11-05 14:16:57 +01:00
parent 65aa53890f
commit b71645a7a2
30 changed files with 1113 additions and 961 deletions

View file

@ -0,0 +1,21 @@
package com.pixelized.desktop.lwa
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.pixelized.desktop.lwa.navigation.MainNavHost
import com.pixelized.desktop.lwa.theme.LwaTheme
import org.jetbrains.compose.ui.tooling.preview.Preview
@Composable
@Preview
fun App() {
LwaTheme {
Surface(
modifier = Modifier.fillMaxSize()
) {
MainNavHost()
}
}
}

View file

@ -0,0 +1,45 @@
package com.pixelized.desktop.lwa.composable.decoratedBox
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.theme.LwaTheme
import org.jetbrains.compose.ui.tooling.preview.Preview
@Composable
fun DecoratedBox(
modifier: Modifier = Modifier,
border: Color = Color(0xFFDFDFDF),
content: @Composable BoxScope.() -> Unit,
) {
Box(
modifier = Modifier
.border(width = 1.dp, color = border, shape = RoundedCornerShape(size = 16.dp))
.padding(all = 2.dp)
.border(width = 1.dp, color = border, shape = RectangleShape)
.then(other = modifier),
content = content,
)
}
@Composable
@Preview
private fun DecoratedBoxPreview() {
LwaTheme {
Surface {
DecoratedBox {
Text("test")
}
}
}
}

View file

@ -0,0 +1,67 @@
package com.pixelized.desktop.lwa.composable.overlay
import androidx.compose.animation.AnimatedContent
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.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.blur
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.lifecycle.viewmodel.compose.viewModel
@Stable
data class BlurOverlayTransitionUio(
val blur: State<Dp>,
val background: State<Color>,
)
@Composable
fun BlurOverlay(
viewModel: BlurOverlayViewModel = viewModel { BlurOverlayViewModel() },
overlay: @Composable BoxScope.() -> Unit,
content: @Composable BoxScope.() -> Unit,
) {
val transition = viewModel.transition
Box {
Box(
modifier = Modifier
.fillMaxSize()
.blur(radius = transition.blur.value)
.drawWithContent {
drawContent()
drawRect(color = transition.background.value)
},
content = content,
)
AnimatedContent(
targetState = viewModel.overlay.value,
transitionSpec = {
val enter = fadeIn() + slideInVertically { 64 }
val exit = fadeOut() + slideOutVertically { 64 }
enter togetherWith exit
},
) { roll ->
when (roll) {
true -> Box(
modifier = Modifier.fillMaxSize(),
content = overlay,
)
else -> Box(
modifier = Modifier.fillMaxSize()
)
}
}
}
}

View file

@ -0,0 +1,50 @@
package com.pixelized.desktop.lwa.composable.overlay
import androidx.compose.animation.animateColor
import androidx.compose.animation.core.animateDp
import androidx.compose.animation.core.updateTransition
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModel
class BlurOverlayViewModel : ViewModel() {
private val _overlay = mutableStateOf(false)
val overlay: State<Boolean>
get() = _overlay
val transition: BlurOverlayTransitionUio
@Composable
get() {
val transition = updateTransition(_overlay.value)
val blur = transition.animateDp {
when (it) {
true -> 8.dp
else -> 0.dp
}
}
val background = transition.animateColor {
when (it) {
true -> Color.Black.copy(alpha = 0.6f)
else -> Color.Black.copy(alpha = 0f)
}
}
return remember {
BlurOverlayTransitionUio(
blur = blur,
background = background,
)
}
}
fun show() {
_overlay.value = true
}
fun hide() {
_overlay.value = false
}
}

View file

@ -0,0 +1,36 @@
package com.pixelized.desktop.lwa.navigation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.compositionLocalOf
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.rememberNavController
import com.pixelized.desktop.lwa.navigation.destination.MainDestination
import com.pixelized.desktop.lwa.navigation.destination.composableCharacterSheetEditPage
import com.pixelized.desktop.lwa.navigation.destination.composableCharacterSheetPage
import com.pixelized.desktop.lwa.navigation.destination.composableMainPage
val LocalScreen = compositionLocalOf<NavHostController> {
error("MainNavHost controller is not yet ready")
}
@Composable
fun MainNavHost(
controller: NavHostController = rememberNavController(),
startDestination: String = MainDestination.navigationRoute(),
) {
CompositionLocalProvider(
LocalScreen provides controller,
) {
NavHost(
navController = controller,
startDestination = startDestination,
) {
composableMainPage()
composableCharacterSheetPage()
composableCharacterSheetEditPage()
}
}
}

View file

@ -0,0 +1,48 @@
package com.pixelized.desktop.lwa.navigation.destination
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import androidx.navigation.navArgument
import com.pixelized.desktop.lwa.screen.characterSheet.detail.CharacterSheetPage
import com.pixelized.desktop.lwa.utils.extention.ARG
object CharacterSheetDestination {
private const val ROUTE = "character.sheet"
private const val CHARACTER_ID = "id"
fun baseRoute() = "$ROUTE?${CHARACTER_ID.ARG}"
fun navigationRoute(id: String) = "$ROUTE?$CHARACTER_ID=$id"
fun arguments() = listOf(
navArgument(CHARACTER_ID) {
nullable = true
}
)
data class Argument(
val id: String,
) {
constructor(savedStateHandle: SavedStateHandle) : this(
id = savedStateHandle.get<String>(CHARACTER_ID) ?: error("missing character id")
)
}
}
fun NavGraphBuilder.composableCharacterSheetPage() {
composable(
route = CharacterSheetDestination.baseRoute(),
arguments = CharacterSheetDestination.arguments(),
) {
CharacterSheetPage()
}
}
fun NavHostController.navigateToCharacterSheet(
id: String,
) {
val route = CharacterSheetDestination.navigationRoute(id = id)
navigate(route = route)
}

View file

@ -0,0 +1,48 @@
package com.pixelized.desktop.lwa.navigation.destination
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import androidx.navigation.navArgument
import com.pixelized.desktop.lwa.screen.characterSheet.edit.CharacterSheetEditPage
import com.pixelized.desktop.lwa.utils.extention.ARG
object CharacterSheetEditDestination {
private const val ROUTE = "character.sheet.edit"
private const val CHARACTER_ID = "id"
fun baseRoute() = "$ROUTE?${CHARACTER_ID.ARG}"
fun navigationRoute(id: String?) = "$ROUTE?$CHARACTER_ID=$id"
fun arguments() = listOf(
navArgument(CHARACTER_ID) {
nullable = true
}
)
data class Argument(
val id: String?,
) {
constructor(savedStateHandle: SavedStateHandle) : this(
id = savedStateHandle.get<String>(CHARACTER_ID)
)
}
}
fun NavGraphBuilder.composableCharacterSheetEditPage() {
composable(
route = CharacterSheetEditDestination.baseRoute(),
arguments = CharacterSheetEditDestination.arguments(),
) {
CharacterSheetEditPage()
}
}
fun NavHostController.navigateToCharacterSheetEdit(
id: String? = null,
) {
val route = CharacterSheetEditDestination.navigationRoute(id = id)
navigate(route = route)
}

View file

@ -0,0 +1,26 @@
package com.pixelized.desktop.lwa.navigation.destination
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import com.pixelized.desktop.lwa.screen.main.MainPage
object MainDestination {
private const val ROUTE = "main"
fun baseRoute() = ROUTE
fun navigationRoute() = ROUTE
}
fun NavGraphBuilder.composableMainPage() {
composable(
route = MainDestination.baseRoute(),
) {
MainPage()
}
}
fun NavHostController.navigateToMainPage() {
val route = MainDestination.navigationRoute()
navigate(route = route)
}

View file

@ -10,28 +10,31 @@ import kotlinx.coroutines.flow.stateIn
object CharacterSheetRepository {
private val scope = CoroutineScope(Dispatchers.IO)
private val preferences = CharacterSheetPreference(
dataStore = createDataStore { "characterssheet.preferences_pb" }
)
fun characterSheet(): StateFlow<List<CharacterSheet>> {
return preferences.loadFlow()
.stateIn(
scope = scope,
started = SharingStarted.Eagerly,
initialValue = emptyList()
)
private val sheets = preferences.loadFlow()
.stateIn(
scope = scope,
started = SharingStarted.Eagerly,
initialValue = emptyList()
)
fun characterSheetFlow(): StateFlow<List<CharacterSheet>> {
return sheets
}
fun characterSheet(id: String): StateFlow<CharacterSheet?> {
return preferences.loadFlow()
fun characterSheetFlow(id: String?): StateFlow<CharacterSheet?> {
return sheets
.map { sheets ->
sheets.firstOrNull { sheet -> sheet.id == id }
}
.stateIn(
scope = scope,
started = SharingStarted.Eagerly,
initialValue = null
initialValue = sheets.value.firstOrNull { it.id == id }
)
}
@ -48,5 +51,11 @@ object CharacterSheetRepository {
// save the list of characters sheet.
preferences.save(sheets = savedSheets)
}
suspend fun delete(id: String) {
val savedSheets = preferences.load().toMutableList()
savedSheets.removeIf { it.id == id }
preferences.save(sheets = savedSheets)
}
}

View file

@ -0,0 +1,70 @@
package com.pixelized.desktop.lwa.screen.characterSheet.detail
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheet
import com.pixelized.desktop.lwa.screen.characterSheet.detail.CharacterSheetPageUio.Characteristic
import com.pixelized.desktop.lwa.screen.characterSheet.detail.CharacterSheetPageUio.Node
class CharacterSheetFactory {
fun convertToUio(model: CharacterSheet): CharacterSheetPageUio {
return CharacterSheetPageUio(
id = model.id,
name = model.name,
characteristics = listOf(
Characteristic(label = "Force", value = "${model.strength}"),
Characteristic(label = "Dextérité", value = "${model.dexterity}"),
Characteristic(label = "Constitution", value = "${model.constitution}"),
Characteristic(label = "Taille", value = "${model.height}"),
Characteristic(label = "Intelligence", value = "${model.intelligence}"),
Characteristic(label = "Pouvoir", value = "${model.power}"),
Characteristic(label = "Charisme", value = "${model.charisma}"),
),
subCharacteristics = listOf(
Characteristic(label = "Déplacement ", value = "${model.movement}"),
Characteristic(
label = "Points de vie",
value = "${model.currentHp}/${model.maxHp}"
),
Characteristic(
label = "Points de pouvoir",
value = "${model.currentPP}/${model.maxPP}"
),
Characteristic(label = "Bonus aux dégâts", value = model.damageBonus),
Characteristic(label = "Armure", value = "${model.armor}"),
),
skills = model.skills.mapNotNull {
if (it.value > 0) {
Node(
type = Node.Type.SKILLS,
label = it.label,
value = it.value,
)
} else {
null
}
},
occupations = model.occupations.mapNotNull {
if (it.value > 0) {
Node(
type = Node.Type.OCCUPATIONS,
label = it.label,
value = it.value,
)
} else {
null
}
},
magics = model.magics.mapNotNull {
if (it.value > 0) {
Node(
type = Node.Type.MAGICS,
label = it.label,
value = it.value,
)
} else {
null
}
},
)
}
}

View file

@ -0,0 +1,377 @@
package com.pixelized.desktop.lwa.screen.characterSheet.detail
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Checkbox
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.lifecycle.createSavedStateHandle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.pixelized.desktop.lwa.composable.decoratedBox.DecoratedBox
import com.pixelized.desktop.lwa.composable.overlay.BlurOverlay
import com.pixelized.desktop.lwa.composable.overlay.BlurOverlayViewModel
import com.pixelized.desktop.lwa.navigation.LocalScreen
import com.pixelized.desktop.lwa.navigation.destination.navigateToCharacterSheetEdit
import com.pixelized.desktop.lwa.screen.roll.RollPage
import com.pixelized.desktop.lwa.screen.roll.RollViewModel
import kotlinx.coroutines.launch
@Stable
data class CharacterSheetPageUio(
val id: String,
val name: String,
val characteristics: List<Characteristic>,
val subCharacteristics: List<Characteristic>,
val skills: List<Node>,
val occupations: List<Node>,
val magics: List<Node>,
) {
@Stable
data class Characteristic(
val label: String,
val value: String,
)
@Stable
data class Node(
val type: Type,
val label: String,
val value: Int,
) {
@Stable
enum class Type {
SKILLS, OCCUPATIONS, MAGICS,
}
}
}
@Composable
fun CharacterSheetPage(
viewModel: CharacterSheetViewModel = viewModel {
CharacterSheetViewModel(savedStateHandle = createSavedStateHandle())
},
overlayViewModel: BlurOverlayViewModel = viewModel { BlurOverlayViewModel() },
rollViewModel: RollViewModel = viewModel { RollViewModel() },
) {
val screen = LocalScreen.current
val scope = rememberCoroutineScope()
Surface(
modifier = Modifier.fillMaxSize(),
) {
BlurOverlay(
viewModel = overlayViewModel,
overlay = {
RollPage(
viewModel = rollViewModel,
onDismissRequest = overlayViewModel::hide,
)
},
content = {
viewModel.sheet.value?.let {
CharacterSheetPageContent(
modifier = Modifier.fillMaxSize(),
characterSheet = it,
onBack = {
screen.popBackStack()
},
onEdit = {
screen.navigateToCharacterSheetEdit(id = it.id)
},
onDelete = {
scope.launch {
viewModel.deleteCharacter(id = it.id)
screen.popBackStack()
}
},
onCharacteristic = { characteristic ->
rollViewModel.prepareRoll(characteristic = characteristic)
overlayViewModel.show()
},
onSkill = { node ->
rollViewModel.prepareRoll(node = node)
overlayViewModel.show()
},
)
}
},
)
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun CharacterSheetPageContent(
modifier: Modifier = Modifier,
scrollState: ScrollState = rememberScrollState(),
width: Dp = 320.dp,
characterSheet: CharacterSheetPageUio,
onBack: () -> Unit,
onEdit: () -> Unit,
onDelete: () -> Unit,
onCharacteristic: (characteristic: CharacterSheetPageUio.Characteristic) -> Unit,
onSkill: (skill: CharacterSheetPageUio.Node) -> Unit,
) {
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = characterSheet.name,
)
},
actions = {
IconButton(
onClick = onEdit,
) {
Icon(
imageVector = Icons.Default.Edit,
contentDescription = null,
)
}
IconButton(
onClick = onDelete,
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = null,
)
}
},
navigationIcon = {
IconButton(
onClick = onBack,
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = null,
)
}
}
)
},
content = { paddingValues ->
Column(
modifier = Modifier
.verticalScroll(state = scrollState).padding(all = 16.dp)
.padding(paddingValues)
.then(other = modifier),
verticalArrangement = Arrangement.spacedBy(space = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
FlowRow(
maxItemsInEachRow = 3,
horizontalArrangement = Arrangement.spacedBy(
space = 16.dp,
alignment = Alignment.CenterHorizontally,
),
verticalArrangement = Arrangement.spacedBy(space = 16.dp),
) {
characterSheet.characteristics.forEach {
Stat(
modifier = Modifier.width(width = width / 3 - 32.dp)
.height(height = 112.dp),
characteristic = it,
onClick = { onCharacteristic(it) },
)
}
}
DecoratedBox(
modifier = Modifier.width(width = width).padding(vertical = 8.dp),
) {
Column {
Text(
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp),
style = MaterialTheme.typography.caption,
textAlign = TextAlign.Center,
text = "Charactéristiques dérivées"
)
characterSheet.subCharacteristics.forEach {
Characteristics(
modifier = Modifier.fillMaxWidth(),
characteristic = it,
)
}
}
}
DecoratedBox(
modifier = Modifier.width(width = width).padding(vertical = 8.dp),
) {
Column {
Text(
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp),
style = MaterialTheme.typography.caption,
textAlign = TextAlign.Center,
text = "Compétences",
)
characterSheet.skills.forEach {
Skill(
modifier = Modifier.fillMaxWidth(),
label = it.label,
value = it.value,
onClick = { onSkill(it) },
)
}
}
}
DecoratedBox(
modifier = Modifier.width(width = width).padding(vertical = 8.dp),
) {
Column {
Text(
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp),
style = MaterialTheme.typography.caption,
textAlign = TextAlign.Center,
text = "Occupations"
)
characterSheet.occupations.forEach {
Skill(
modifier = Modifier.fillMaxWidth(),
label = it.label,
value = it.value,
onClick = { onSkill(it) },
)
}
}
}
DecoratedBox(
modifier = Modifier.width(width = width).padding(vertical = 8.dp),
) {
Column {
Text(
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp),
style = MaterialTheme.typography.caption,
textAlign = TextAlign.Center,
text = "Compétences magiques"
)
characterSheet.magics.forEach {
Skill(
modifier = Modifier.fillMaxWidth(),
label = it.label,
value = it.value,
onClick = { onSkill(it) },
)
}
}
}
}
}
)
}
@Composable
private fun Stat(
modifier: Modifier = Modifier,
paddingValues: PaddingValues = PaddingValues(all = 8.dp),
characteristic: CharacterSheetPageUio.Characteristic,
onClick: () -> Unit,
) {
DecoratedBox(
modifier = Modifier.clickable(onClick = onClick).padding(paddingValues = paddingValues)
.then(other = modifier),
) {
Text(
modifier = Modifier.align(alignment = Alignment.TopCenter),
style = MaterialTheme.typography.caption,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = characteristic.label,
)
Text(
modifier = Modifier.align(alignment = Alignment.Center),
style = MaterialTheme.typography.h4,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = characteristic.value
)
}
}
@Composable
private fun Characteristics(
modifier: Modifier = Modifier,
paddingValues: PaddingValues = PaddingValues(horizontal = 8.dp),
characteristic: CharacterSheetPageUio.Characteristic,
) {
Row(
modifier = Modifier.padding(paddingValues = paddingValues).then(other = modifier),
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.body1,
text = characteristic.label
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.body1,
fontWeight = FontWeight.Bold,
text = characteristic.value,
)
}
}
@Composable
private fun Skill(
modifier: Modifier = Modifier,
paddingValues: PaddingValues = PaddingValues(horizontal = 8.dp),
label: String,
value: Any,
onClick: () -> Unit,
) {
Row(
modifier = Modifier.clickable(onClick = onClick).padding(paddingValues = paddingValues)
.then(other = modifier),
horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier.weight(1f),
style = MaterialTheme.typography.body1,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = label
)
Text(
style = MaterialTheme.typography.body1,
fontWeight = FontWeight.Bold,
text = "$value",
)
Checkbox(modifier = Modifier.size(size = 32.dp), checked = false, onCheckedChange = { })
}
}

View file

@ -0,0 +1,32 @@
package com.pixelized.desktop.lwa.screen.characterSheet.detail
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import com.pixelized.desktop.lwa.navigation.destination.CharacterSheetDestination
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.desktop.lwa.utils.extention.collectAsState
class CharacterSheetViewModel(
savedStateHandle: SavedStateHandle,
) : ViewModel() {
private val argument = CharacterSheetDestination.Argument(savedStateHandle)
private val repository = CharacterSheetRepository
private val factory = CharacterSheetFactory()
val sheet: State<CharacterSheetPageUio?>
@Composable
@Stable
get() = repository
.characterSheetFlow(id = argument.id)
.collectAsState { sheet ->
sheet?.let { model -> factory.convertToUio(model = model) }
}
suspend fun deleteCharacter(id: String) {
repository.delete(id = id)
}
}

View file

@ -0,0 +1,196 @@
package com.pixelized.desktop.lwa.screen.characterSheet.edit
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Add
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.createSavedStateHandle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.pixelized.desktop.lwa.composable.decoratedBox.DecoratedBox
import com.pixelized.desktop.lwa.navigation.LocalScreen
import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.FieldUio
import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.Form
import kotlinx.coroutines.launch
@Stable
data class CharacterSheetEditPageUio(
val id: String,
val name: FieldUio,
val skills: List<SkillGroup>,
) {
@Stable
data class SkillGroup(
val title: String,
val type: Type,
val editable: Boolean = false,
val fields: List<FieldUio>,
) {
@Stable
enum class Type {
CHARACTERISTICS,
SUB_CHARACTERISTICS,
SKILLS,
OCCUPATIONS,
MAGICS,
OTHER,
}
}
}
@Composable
fun CharacterSheetEditPage(
viewModel: CharacterSheetEditViewModel = viewModel {
CharacterSheetEditViewModel(
savedStateHandle = createSavedStateHandle()
)
},
) {
val screen = LocalScreen.current
val scope = rememberCoroutineScope()
Surface(
modifier = Modifier.fillMaxSize(),
) {
CharacterSheetEdit(
form = viewModel.characterSheet.value,
onSkill = viewModel::onSkill,
onBack = { screen.popBackStack() },
onSave = {
scope.launch {
viewModel.save()
screen.popBackStack()
}
},
)
}
}
@Composable
fun CharacterSheetEdit(
form: CharacterSheetEditPageUio,
onSkill: (CharacterSheetEditPageUio.SkillGroup) -> Unit,
onBack: () -> Unit,
onSave: () -> Unit,
) {
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = "Création de personnage",
)
},
navigationIcon = {
IconButton(
onClick = onBack,
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = null,
)
}
}
)
},
content = { paddingValues ->
Column(
modifier = Modifier
.verticalScroll(state = rememberScrollState())
.padding(paddingValues = paddingValues)
.padding(all = 24.dp),
verticalArrangement = Arrangement.spacedBy(space = 16.dp)
) {
Form(
modifier = Modifier.fillMaxWidth(),
field = form.name,
)
form.skills.forEach {
DecoratedBox(
modifier = Modifier.animateContentSize(),
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
modifier = Modifier.padding(vertical = 8.dp),
style = MaterialTheme.typography.caption,
text = it.title,
)
it.fields.forEach {
Form(
modifier = Modifier.fillMaxWidth(),
field = it,
)
}
if (it.editable) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(
space = 8.dp,
alignment = Alignment.End
)
) {
TextButton(
onClick = { onSkill(it) },
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
) {
Text(
text = "Ajouter une ligne",
)
Icon(
imageVector = Icons.Default.Add,
contentDescription = null,
)
}
}
}
}
}
}
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End,
) {
TextButton(
onClick = onSave,
) {
Text(text = "Sauvegarder")
}
}
}
}
)
}

View file

@ -0,0 +1,64 @@
package com.pixelized.desktop.lwa.screen.characterSheet.edit
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import com.pixelized.desktop.lwa.navigation.destination.CharacterSheetEditDestination
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.desktop.lwa.screen.characterSheet.edit.CharacterSheetEditPageUio.SkillGroup
import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.FieldUio
class CharacterSheetEditViewModel(
savedStateHandle: SavedStateHandle,
) : ViewModel() {
private val argument = CharacterSheetEditDestination.Argument(savedStateHandle)
private val repository = CharacterSheetRepository
private val factory = CharacterSheetFactory()
private val _characterSheet = repository
.characterSheetFlow(id = argument.id).value
.let { sheet -> mutableStateOf(factory.convertToUio(sheet = sheet)) }
val characterSheet: State<CharacterSheetEditPageUio> get() = _characterSheet
fun onSkill(skill: SkillGroup) {
val sheet = _characterSheet.value
_characterSheet.value = sheet.copy(
skills = sheet.skills.map { group ->
if (skill.title == group.title) {
group.copy(
fields = mutableListOf<FieldUio>().apply {
addAll(group.fields)
add(
FieldUio.create(
label = "",
isLabelEditable = true,
valuePlaceHolder = {
when (group.type) {
SkillGroup.Type.CHARACTERISTICS -> ""
SkillGroup.Type.SUB_CHARACTERISTICS -> ""
SkillGroup.Type.SKILLS -> "0"
SkillGroup.Type.OCCUPATIONS -> "40"
SkillGroup.Type.MAGICS -> "0"
SkillGroup.Type.OTHER -> ""
}
},
)
)
}
)
} else {
group
}
}
)
}
suspend fun save() {
val sheet = _characterSheet.value
val model = factory.convertToModel(sheet = sheet)
repository.save(characterSheet = model)
}
}

View file

@ -0,0 +1,268 @@
package com.pixelized.desktop.lwa.screen.characterSheet.edit
import com.pixelized.desktop.lwa.business.SkillNormalizerUseCase.normalize
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheet
import com.pixelized.desktop.lwa.screen.characterSheet.edit.CharacterSheetEditPageUio.SkillGroup
import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.FieldUio
import java.util.UUID
import kotlin.math.max
class CharacterSheetFactory {
fun convertToModel(sheet: CharacterSheetEditPageUio): CharacterSheet {
return CharacterSheet(
id = sheet.id,
name = sheet.name.value.value,
strength = sheet.skills[0].fields[0].unpack(),
dexterity = sheet.skills[0].fields[1].unpack(),
constitution = sheet.skills[0].fields[2].unpack(),
height = sheet.skills[0].fields[3].unpack(),
intelligence = sheet.skills[0].fields[4].unpack(),
power = sheet.skills[0].fields[5].unpack(),
charisma = sheet.skills[0].fields[6].unpack(),
movement = sheet.skills[1].fields[0].unpack(),
currentHp = sheet.skills[1].fields[1].unpack(),
maxHp = sheet.skills[1].fields[1].unpack(),
currentPP = sheet.skills[1].fields[2].unpack(),
maxPP = sheet.skills[1].fields[2].unpack(),
damageBonus = sheet.skills[1].fields[3].unpack(),
armor = sheet.skills[1].fields[4].unpack(),
skills = sheet.skills[2].fields.map {
CharacterSheet.Skill(
label = it.label.value,
value = it.unpack(),
used = false,
)
},
occupations = sheet.skills[3].fields.map {
CharacterSheet.Skill(
label = it.label.value,
value = it.unpack(),
used = false,
)
},
magics = sheet.skills[4].fields.map {
CharacterSheet.Skill(
label = it.label.value,
value = it.unpack(),
used = false,
)
},
attacks = emptyList(),
)
}
fun convertToUio(
sheet: CharacterSheet?,
): CharacterSheetEditPageUio {
val str = FieldUio.create(
label = "Force",
valuePlaceHolder = { "0" },
initialValue = sheet?.strength?.toString() ?: ""
)
val dex = FieldUio.create(
label = "Dextérité",
valuePlaceHolder = { "0" },
initialValue = sheet?.dexterity?.toString() ?: ""
)
val con = FieldUio.create(
label = "Constitution",
valuePlaceHolder = { "0" },
initialValue = sheet?.constitution?.toString() ?: ""
)
val hei = FieldUio.create(
label = "Taille",
valuePlaceHolder = { "0" },
initialValue = sheet?.height?.toString() ?: ""
)
val int = FieldUio.create(
label = "Intelligence",
valuePlaceHolder = { "0" },
initialValue = sheet?.intelligence?.toString() ?: ""
)
val pow = FieldUio.create(
label = "Pouvoir",
valuePlaceHolder = { "0" },
initialValue = sheet?.power?.toString() ?: ""
)
val cha = FieldUio.create(
label = "Charisme",
valuePlaceHolder = { "0" },
initialValue = sheet?.charisma?.toString() ?: ""
)
fun str(): Int = str.unpack() ?: 0
fun dex(): Int = dex.unpack() ?: 0
fun con(): Int = con.unpack() ?: 0
fun hei(): Int = hei.unpack() ?: 0
fun int(): Int = int.unpack() ?: 0
fun pow(): Int = pow.unpack() ?: 0
fun cha(): Int = cha.unpack() ?: 0
return CharacterSheetEditPageUio(
id = sheet?.id ?: UUID.randomUUID().toString(),
name = FieldUio.create(
useLabelAsPlaceholder = true,
label = "Name",
initialValue = sheet?.name ?: ""
),
skills = listOf(
SkillGroup(
title = "Charactéristiques",
type = SkillGroup.Type.CHARACTERISTICS,
fields = listOf(str, dex, con, hei, int, pow, cha),
),
SkillGroup(
title = "Charactéristiques dérivées",
type = SkillGroup.Type.SUB_CHARACTERISTICS,
fields = listOf(
FieldUio.create(
label = "Déplacement",
valuePlaceHolder = { "10" },
initialValue = sheet?.movement?.toString() ?: ""
),
FieldUio.create(
label = "Points de vie",
valuePlaceHolder = { "${(con() + hei()) / 2}" },
initialValue = sheet?.maxHp?.toString() ?: ""
),
FieldUio.create(
label = "Points de pouvoir",
valuePlaceHolder = { "${pow()}" },
initialValue = sheet?.maxPP?.toString() ?: ""
),
FieldUio.create(
label = "Bonus aux dégats",
valuePlaceHolder = {
val bonus = str() + hei()
when {
bonus < 12 -> "-1d6"
bonus in 12..17 -> "-1d4"
bonus in 18..22 -> "-0"
bonus in 23..29 -> "1d4"
bonus in 30..39 -> "1d6"
else -> "2d6"
}
},
initialValue = sheet?.damageBonus ?: ""
),
FieldUio.create(
label = "Armure",
valuePlaceHolder = { "0" },
initialValue = sheet?.armor?.toString() ?: ""
),
),
),
SkillGroup(
title = "Compétances",
type = SkillGroup.Type.SKILLS,
editable = true,
fields = sheet?.skills?.map {
FieldUio.create(
label = it.label,
valuePlaceHolder = { "" },
initialValue = it.value.toString(),
)
} ?: listOf(
FieldUio.create(
label = "Bagarre",
valuePlaceHolder = { "${normalize(dex() * 2)}" },
),
FieldUio.create(
label = "Esquive",
valuePlaceHolder = { "${normalize(dex() * 2)}" },
),
FieldUio.create(
label = "Saisie",
valuePlaceHolder = { "${normalize(str() + hei())}" },
),
FieldUio.create(
label = "Lancer",
valuePlaceHolder = { "${normalize(str() + dex())}" },
),
FieldUio.create(
label = "Athlétisme",
valuePlaceHolder = { "${normalize(str() + con() * 2)}" },
),
FieldUio.create(
label = "Acrobatie",
valuePlaceHolder = { "${normalize(dex() + con() * 2)}" },
),
FieldUio.create(
label = "Perception",
valuePlaceHolder = { "${normalize(10 + int() * 2)}" },
),
FieldUio.create(
label = "Recherche",
valuePlaceHolder = { "${normalize(10 + int() * 2)}" },
),
FieldUio.create(
label = "Empathie",
valuePlaceHolder = { "${normalize(cha() + int())}" },
),
FieldUio.create(
label = "Persuasion",
valuePlaceHolder = { "${normalize(cha() * 3)}" },
),
FieldUio.create(
label = "Intimidation",
valuePlaceHolder = { "${normalize(cha() + max(pow(), hei()) * 2)}" },
),
FieldUio.create(
label = "Baratin",
valuePlaceHolder = { "${normalize(cha() * 2 + int())}" },
),
FieldUio.create(
label = "Marchandage",
valuePlaceHolder = { "${normalize(cha() * 2)}" },
),
FieldUio.create(
label = "Discrétion",
valuePlaceHolder = { "${normalize(cha() + dex() * 2 - hei())}" },
),
FieldUio.create(
label = "Escamotage",
valuePlaceHolder = { "${normalize(dex() * 2)}" },
),
FieldUio.create(
label = "Premiers soins",
valuePlaceHolder = { "${normalize(int() + dex())}" },
),
),
),
SkillGroup(
title = "Occupations",
type = SkillGroup.Type.OCCUPATIONS,
editable = true,
fields = sheet?.occupations?.map {
FieldUio.create(
label = it.label,
valuePlaceHolder = { "40" },
initialValue = it.value.toString()
)
} ?: emptyList(),
),
SkillGroup(
title = "Compétences magiques",
type = SkillGroup.Type.MAGICS,
editable = true,
fields = sheet?.magics?.map {
FieldUio.create(
label = it.label,
valuePlaceHolder = { "" },
initialValue = it.value.toString()
)
} ?: emptyList(),
),
)
)
}
private inline fun <reified T> FieldUio.unpack(): T {
val tmp = value.value.ifBlank { valuePlaceHolder.value }
return when (T::class) {
Int::class -> (tmp.toIntOrNull() ?: 0) as T
else -> tmp as T
}
}
}

View file

@ -0,0 +1,125 @@
package com.pixelized.desktop.lwa.screen.characterSheet.edit.composable
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@Stable
open class FieldUio(
val useLabelAsPlaceholder: Boolean,
val isLabelEditable: Boolean,
val label: State<String>,
val onLabelChange: (String) -> Unit,
val valuePlaceHolder: State<String>,
val value: State<String>,
val onValueChange: (String) -> Unit,
) {
companion object {
@Stable
fun create(
useLabelAsPlaceholder: Boolean = false,
isLabelEditable: Boolean = false,
label: String = "",
initialValue: String = "",
valuePlaceHolder: () -> String = { "" },
): FieldUio {
val labelState = mutableStateOf(label)
val valueState = mutableStateOf(initialValue)
return FieldUio(
useLabelAsPlaceholder = useLabelAsPlaceholder,
isLabelEditable = useLabelAsPlaceholder.not() && isLabelEditable,
label = labelState,
onLabelChange = { labelState.value = it },
valuePlaceHolder = derivedStateOf(valuePlaceHolder),
value = valueState,
onValueChange = { valueState.value = it },
)
}
}
}
@Composable
fun Form(
modifier: Modifier = Modifier,
field: FieldUio,
) {
val focus = LocalFocusManager.current
AnimatedContent(
targetState = field.useLabelAsPlaceholder,
transitionSpec = { fadeIn() togetherWith fadeOut() }
) {
when (it) {
true -> {
TextField(
modifier = modifier,
value = field.value.value,
label = { Text(text = field.label.value) },
singleLine = true,
keyboardActions = KeyboardActions { focus.moveFocus(FocusDirection.Next) },
onValueChange = field.onValueChange,
)
}
else -> {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
AnimatedContent(
modifier = Modifier.weight(weight = 1f),
targetState = field.isLabelEditable,
transitionSpec = { fadeIn() togetherWith fadeOut() },
) { editable ->
when (editable) {
true -> TextField(
value = field.label.value,
placeholder = { Text(text = "Nom") },
singleLine = true,
keyboardActions = KeyboardActions { focus.moveFocus(FocusDirection.Next) },
onValueChange = field.onLabelChange,
)
else -> Text(
modifier = Modifier.padding(horizontal = 16.dp),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.body1,
text = field.label.value
)
}
}
TextField(
modifier = Modifier.width(width = 80.dp),
value = field.value.value,
placeholder = { Text(text = field.valuePlaceHolder.value) },
singleLine = true,
keyboardActions = KeyboardActions { focus.moveFocus(FocusDirection.Next) },
onValueChange = field.onValueChange,
)
}
}
}
}
}

View file

@ -0,0 +1,95 @@
package com.pixelized.desktop.lwa.screen.main
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.pixelized.desktop.lwa.navigation.LocalScreen
import com.pixelized.desktop.lwa.navigation.destination.navigateToCharacterSheet
import com.pixelized.desktop.lwa.navigation.destination.navigateToCharacterSheetEdit
@Stable
data class CharacterUio(
val id: String,
val name: String,
)
@Composable
fun MainPage(
viewModel: MainPageViewModel = viewModel { MainPageViewModel() },
) {
val screen = LocalScreen.current
Surface {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
MainPageContent(
characters = viewModel.characters,
onCharacter = {
screen.navigateToCharacterSheet(id = it.id)
},
onCreateCharacter = {
screen.navigateToCharacterSheetEdit()
},
)
}
}
}
@Composable
fun MainPageContent(
modifier: Modifier = Modifier,
characters: State<List<CharacterUio>>,
onCharacter: (CharacterUio) -> Unit,
onCreateCharacter: () -> Unit,
) {
Column(
modifier = modifier.padding(horizontal = 24.dp),
verticalArrangement = Arrangement.spacedBy(space = 32.dp),
) {
Column {
characters.value.forEach { sheet ->
TextButton(
onClick = { onCharacter(sheet) }
) {
Text(
modifier = Modifier.fillMaxWidth(),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Start,
text = sheet.name,
)
}
}
}
TextButton(
onClick = { onCreateCharacter() },
) {
Text(
modifier = Modifier.fillMaxWidth(),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Start,
text = "Créer une feuille de personnage",
)
}
}
}

View file

@ -0,0 +1,27 @@
package com.pixelized.desktop.lwa.screen.main
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.lifecycle.ViewModel
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.desktop.lwa.utils.extention.collectAsState
class MainPageViewModel : ViewModel() {
// using a variable to help with later injection.
private val characterSheetRepository = CharacterSheetRepository
val characters: State<List<CharacterUio>>
@Composable
@Stable
get() = characterSheetRepository
.characterSheetFlow()
.collectAsState { sheets ->
sheets.map { sheet ->
CharacterUio(
id = sheet.id,
name = sheet.name,
)
}
}
}

View file

@ -0,0 +1,150 @@
package com.pixelized.desktop.lwa.screen.roll
import androidx.compose.animation.AnimatedContent
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.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import kotlinx.coroutines.launch
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.ic_d20_32dp
import org.jetbrains.compose.resources.painterResource
@Stable
data class RollUio(
val label: String,
val value: Int,
)
@Stable
data class RollResultUio(
val label: String,
val value: Int,
)
@Composable
fun RollPage(
viewModel: RollViewModel = viewModel { RollViewModel() },
onDismissRequest: () -> Unit,
) {
val scope = rememberCoroutineScope()
Column(
modifier = Modifier.fillMaxSize()
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = onDismissRequest,
)
.padding(all = 32.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
style = MaterialTheme.typography.caption,
textAlign = TextAlign.Center,
overflow = TextOverflow.Ellipsis,
text = "Jet de :",
)
Text(
style = MaterialTheme.typography.h5,
textAlign = TextAlign.Center,
overflow = TextOverflow.Ellipsis,
text = viewModel.roll.value.label,
)
Text(
style = MaterialTheme.typography.caption,
textAlign = TextAlign.Center,
overflow = TextOverflow.Ellipsis,
text = "Réussite en dessous de : ${viewModel.roll.value.value}",
)
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(
space = 24.dp,
alignment = Alignment.CenterVertically,
),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Box(
contentAlignment = Alignment.Center
) {
Icon(
modifier = Modifier
.clip(shape = CircleShape)
.clickable { scope.launch { viewModel.roll() } }
.padding(all = 24.dp)
.size(size = 128.dp)
.graphicsLayer {
this.alpha = 0.8f
this.rotationZ = viewModel.rollRotation.value
},
tint = MaterialTheme.colors.onSurface,
painter = painterResource(Res.drawable.ic_d20_32dp),
contentDescription = null,
)
AnimatedContent(
targetState = viewModel.result.value?.value?.toString() ?: "",
transitionSpec = {
val enter = fadeIn() + slideInVertically { 32 }
val exit = fadeOut() + slideOutVertically { -32 }
enter togetherWith exit
}
) { label ->
Text(
modifier = Modifier.width(width = 128.dp),
style = MaterialTheme.typography.h3.copy(
shadow = Shadow(
color = MaterialTheme.colors.surface,
offset = Offset.Zero,
blurRadius = 8f,
)
),
textAlign = TextAlign.Center,
color = MaterialTheme.colors.onSurface,
text = label,
)
}
}
AnimatedContent(
targetState = viewModel.result.value?.label ?: "",
transitionSpec = { fadeIn() togetherWith fadeOut() },
) { value ->
Text(
modifier = Modifier.fillMaxWidth(),
style = MaterialTheme.typography.h4,
textAlign = TextAlign.Center,
text = value,
)
}
}
}
}

View file

@ -0,0 +1,98 @@
package com.pixelized.desktop.lwa.screen.roll
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import com.pixelized.desktop.lwa.business.RollUseCase
import com.pixelized.desktop.lwa.business.SkillStepUseCase
import com.pixelized.desktop.lwa.screen.characterSheet.detail.CharacterSheetPageUio
import kotlinx.coroutines.Job
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
class RollViewModel : ViewModel() {
private val _roll = mutableStateOf(RollUio(label = "", value = 0))
val roll: State<RollUio> get() = _roll
private var rollJob: Job? = null
private lateinit var rollStep: SkillStepUseCase.SkillStep
val rollRotation = Animatable(0f)
private val _result = mutableStateOf<RollResultUio?>(null)
val result: State<RollResultUio?> get() = _result
fun prepareRoll(node: CharacterSheetPageUio.Node) {
val step = SkillStepUseCase.computeSkillStep(
skill = node.value,
)
prepareRoll(
label = node.label,
step = step,
)
}
fun prepareRoll(characteristic: CharacterSheetPageUio.Characteristic) {
val step = SkillStepUseCase.computeSkillStep(
skill = (characteristic.value.toIntOrNull() ?: 0) * 5
)
prepareRoll(
label = characteristic.label,
step = step,
)
}
private fun prepareRoll(
label: String,
step: SkillStepUseCase.SkillStep,
) {
runBlocking { rollRotation.snapTo(0f) }
rollStep = step
_result.value = null
_roll.value = RollUio(
label = label,
value = step.successRange.last
)
}
suspend fun roll() {
coroutineScope {
_result.value = null
rollJob?.cancel()
rollJob = launch {
launch {
rollRotation.animateTo(
targetValue = rollRotation.value.let { it - it % 360 } + 360f * 3,
animationSpec = spring(
dampingRatio = Spring.DampingRatioNoBouncy,
stiffness = Spring.StiffnessLow,
)
)
}
launch {
delay(500)
val d100 = RollUseCase.rollD100()
_result.value = RollResultUio(
label = when (d100) {
// TODO wording
in rollStep.criticalSuccessRange -> "Réussite critique"
in rollStep.specialSuccessRange -> "Réussite spéciale"
in rollStep.successRange -> "Réussite"
in rollStep.failureRange -> "Échec"
in rollStep.criticalFailureRange -> "Échec critique"
else -> ""
},
value = d100,
)
}
}
}
}
}

View file

@ -0,0 +1,17 @@
package com.pixelized.desktop.lwa.theme
import androidx.compose.material.MaterialTheme
import androidx.compose.material.darkColors
import androidx.compose.runtime.Composable
@Composable
fun LwaTheme(
content: @Composable () -> Unit
) {
MaterialTheme(
colors = darkColors(),
typography = MaterialTheme.typography,
shapes = MaterialTheme.shapes,
content = content,
)
}

View file

@ -0,0 +1,3 @@
package com.pixelized.desktop.lwa.utils.extention
val String.ARG: String get() = "$this={$this}"