From 3705fbd94741d0b06c51a9542131bd8be41cabc5 Mon Sep 17 00:00:00 2001 From: Thomas Andres Gomez Date: Mon, 11 Nov 2024 22:32:01 +0100 Subject: [PATCH] Add PP and HP managmenet --- .../detail/CharacterSheetFactory.kt | 41 ++++- .../detail/CharacterSheetPage.kt | 44 ++++- .../detail/CharacterSheetViewModel.kt | 68 +++++++- .../CharacterSheetDeleteConfirmationDialog.kt | 2 +- .../detail/dialog/CharacterSheetStatDialog.kt | 158 ++++++++++++++++++ 5 files changed, 305 insertions(+), 8 deletions(-) rename composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/detail/{ => dialog}/CharacterSheetDeleteConfirmationDialog.kt (98%) create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/detail/dialog/CharacterSheetStatDialog.kt diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/detail/CharacterSheetFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/detail/CharacterSheetFactory.kt index e15c67e..46199fd 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/detail/CharacterSheetFactory.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/detail/CharacterSheetFactory.kt @@ -4,66 +4,105 @@ 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 import lwacharactersheet.composeapp.generated.resources.Res -import lwacharactersheet.composeapp.generated.resources.* +import lwacharactersheet.composeapp.generated.resources.character_sheet__characteristics__cha +import lwacharactersheet.composeapp.generated.resources.character_sheet__characteristics__con +import lwacharactersheet.composeapp.generated.resources.character_sheet__characteristics__dex +import lwacharactersheet.composeapp.generated.resources.character_sheet__characteristics__hei +import lwacharactersheet.composeapp.generated.resources.character_sheet__characteristics__int +import lwacharactersheet.composeapp.generated.resources.character_sheet__characteristics__pow import lwacharactersheet.composeapp.generated.resources.character_sheet__characteristics__str +import lwacharactersheet.composeapp.generated.resources.character_sheet__sub_characteristics__armor +import lwacharactersheet.composeapp.generated.resources.character_sheet__sub_characteristics__damage_bonus +import lwacharactersheet.composeapp.generated.resources.character_sheet__sub_characteristics__hit_point +import lwacharactersheet.composeapp.generated.resources.character_sheet__sub_characteristics__movement +import lwacharactersheet.composeapp.generated.resources.character_sheet__sub_characteristics__power_point import org.jetbrains.compose.resources.getString class CharacterSheetFactory { + companion object { + const val HP = "HP" + const val PP = "PP" + } + suspend fun convertToUio(model: CharacterSheet): CharacterSheetPageUio { return CharacterSheetPageUio( id = model.id, name = model.name, characteristics = listOf( Characteristic( + id = "STR", label = getString(Res.string.character_sheet__characteristics__str), value = "${model.strength}", + editable = false, ), Characteristic( + id = "DEX", label = getString(Res.string.character_sheet__characteristics__dex), value = "${model.dexterity}", + editable = false, ), Characteristic( + id = "CON", label = getString(Res.string.character_sheet__characteristics__con), value = "${model.constitution}", + editable = false, ), Characteristic( + id = "HEI", label = getString(Res.string.character_sheet__characteristics__hei), value = "${model.height}", + editable = false, ), Characteristic( + id = "INT", label = getString(Res.string.character_sheet__characteristics__int), value = "${model.intelligence}", + editable = false, ), Characteristic( + id = "POW", label = getString(Res.string.character_sheet__characteristics__pow), value = "${model.power}", + editable = false, ), Characteristic( + id = "CHA", label = getString(Res.string.character_sheet__characteristics__cha), value = "${model.charisma}", + editable = false, ), ), subCharacteristics = listOf( Characteristic( + id = "MOV", label = getString(Res.string.character_sheet__sub_characteristics__movement), value = "${model.movement}", + editable = false, ), Characteristic( + id = HP, label = getString(Res.string.character_sheet__sub_characteristics__hit_point), value = "${model.currentHp}/${model.maxHp}", + editable = true, ), Characteristic( + id = PP, label = getString(Res.string.character_sheet__sub_characteristics__power_point), value = "${model.currentPP}/${model.maxPP}", + editable = true, ), Characteristic( + id = "DMG", label = getString(Res.string.character_sheet__sub_characteristics__damage_bonus), value = model.damageBonus, + editable = false, ), Characteristic( + id = "ARMOR", label = getString(Res.string.character_sheet__sub_characteristics__armor), value = "${model.armor}", + editable = false, ), ), skills = model.skills.mapNotNull { diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/detail/CharacterSheetPage.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/detail/CharacterSheetPage.kt index 6a8f969..6421e67 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/detail/CharacterSheetPage.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/detail/CharacterSheetPage.kt @@ -45,6 +45,8 @@ import com.pixelized.desktop.lwa.composable.overlay.BlurOverlay import com.pixelized.desktop.lwa.composable.overlay.BlurOverlayViewModel import com.pixelized.desktop.lwa.navigation.LocalScreenController import com.pixelized.desktop.lwa.navigation.destination.navigateToCharacterSheetEdit +import com.pixelized.desktop.lwa.screen.characterSheet.detail.dialog.CharacterSheetDeleteConfirmationDialog +import com.pixelized.desktop.lwa.screen.characterSheet.detail.dialog.CharacterSheetStatDialog import com.pixelized.desktop.lwa.screen.roll.RollPage import com.pixelized.desktop.lwa.screen.roll.RollViewModel import kotlinx.coroutines.launch @@ -70,8 +72,10 @@ data class CharacterSheetPageUio( ) { @Stable data class Characteristic( + val id: String, val label: String, val value: String, + val editable: Boolean, ) @Stable @@ -132,6 +136,9 @@ fun CharacterSheetPage( ) overlayViewModel.show() }, + onSubCharacteristic = { + viewModel.showSubCharacteristicDialog(id = it.id) + }, onSkill = { node -> rollViewModel.prepareRoll(sheet = sheet, node = node) overlayViewModel.show() @@ -161,6 +168,20 @@ fun CharacterSheetPage( viewModel.hideConfirmCharacterDeletionDialog() }, ) + + CharacterSheetStatDialog( + dialog = viewModel.statChangeDialog, + onConfirm = { + viewModel.changeSubCharacteristic( + characteristicId = it.id, + value = it.value().text.toIntOrNull() ?: 0, + ) + viewModel.hideSubCharacteristicDialog() + }, + onDismissRequest = { + viewModel.hideSubCharacteristicDialog() + } + ) } @Composable @@ -171,6 +192,7 @@ fun CharacterSheetPageContent( onEdit: () -> Unit, onDelete: () -> Unit, onCharacteristic: (characteristic: CharacterSheetPageUio.Characteristic) -> Unit, + onSubCharacteristic: (characteristic: CharacterSheetPageUio.Characteristic) -> Unit, onSkill: (skill: CharacterSheetPageUio.Node) -> Unit, onUseSkill: (skill: CharacterSheetPageUio.Node) -> Unit, onRoll: (roll: CharacterSheetPageUio.Roll) -> Unit, @@ -252,9 +274,13 @@ fun CharacterSheetPageContent( text = stringResource(Res.string.character_sheet__sub_characteristics__title), ) characterSheet.subCharacteristics.forEach { - Characteristics( + SubCharacteristics( modifier = Modifier.cell(), characteristic = it, + onCharacteristic = when { + it.editable -> onSubCharacteristic + else -> null + } ) } } @@ -372,24 +398,32 @@ private fun Stat( } @Composable -private fun Characteristics( +private fun SubCharacteristics( modifier: Modifier = Modifier, paddingValues: PaddingValues = PaddingValues(horizontal = 8.dp), characteristic: CharacterSheetPageUio.Characteristic, + onCharacteristic: ((characteristic: CharacterSheetPageUio.Characteristic) -> Unit)?, ) { Row( - modifier = Modifier.padding(paddingValues = paddingValues).then(other = modifier), + modifier = Modifier + .clickable(enabled = characteristic.editable && onCharacteristic != null) { + onCharacteristic?.invoke(characteristic) + } + .padding(paddingValues = paddingValues).then(other = modifier), horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, ) { Text( - modifier = Modifier.alignByBaseline(), style = MaterialTheme.typography.body1, text = characteristic.label ) Text( - modifier = Modifier.alignByBaseline(), style = MaterialTheme.typography.body1, fontWeight = FontWeight.Bold, + color = when (characteristic.editable && onCharacteristic != null) { + true -> MaterialTheme.colors.primary + else -> MaterialTheme.colors.onSurface + }, text = characteristic.value, ) } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/detail/CharacterSheetViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/detail/CharacterSheetViewModel.kt index 58eed13..525731b 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/detail/CharacterSheetViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/detail/CharacterSheetViewModel.kt @@ -4,12 +4,18 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.TextFieldValue 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.screen.characterSheet.detail.dialog.CharacterSheetDeleteConfirmationDialogUio +import com.pixelized.desktop.lwa.screen.characterSheet.detail.dialog.StatChangeDialogUio import com.pixelized.desktop.lwa.utils.extention.collectAsState import kotlinx.coroutines.runBlocking +import kotlin.math.max +import kotlin.math.min class CharacterSheetViewModel( savedStateHandle: SavedStateHandle, @@ -24,6 +30,9 @@ class CharacterSheetViewModel( val displayDeleteConfirmationDialog: State get() = _displayDeleteConfirmationDialog + private val _statChangeDialog = mutableStateOf(null) + val statChangeDialog: State get() = _statChangeDialog + val sheet: State @Composable @Stable @@ -35,7 +44,7 @@ class CharacterSheetViewModel( } } - suspend fun deleteCharacter(id: String) { + fun deleteCharacter(id: String) { repository.delete(id = id) } @@ -74,4 +83,61 @@ class CharacterSheetViewModel( fun hideConfirmCharacterDeletionDialog() { _displayDeleteConfirmationDialog.value = null } + + fun showSubCharacteristicDialog(id: String) { + repository.characterSheetFlow(id = argument.id).value?.let { sheet -> + _statChangeDialog.value = when (id) { + CharacterSheetFactory.HP -> { + val value = mutableStateOf( + "${sheet.currentHp}".let { + TextFieldValue(text = it, selection = TextRange(it.length)) + } + ) + StatChangeDialogUio( + id = CharacterSheetFactory.HP, + value = { value.value }, + onValueChange = { value.value = it }, + placeholder = "${sheet.currentHp}", + maxValue = "${sheet.maxHp}", + ) + } + + CharacterSheetFactory.PP -> { + val value = mutableStateOf( + "${sheet.currentPP}".let { + TextFieldValue(text = it, selection = TextRange(it.length)) + } + ) + StatChangeDialogUio( + id = CharacterSheetFactory.PP, + value = { value.value }, + onValueChange = { value.value = it }, + placeholder = "${sheet.currentPP}", + maxValue = "${sheet.maxPP}", + ) + } + + else -> null + } + } + } + + fun hideSubCharacteristicDialog() { + _statChangeDialog.value = null + } + + fun changeSubCharacteristic( + characteristicId: String, + value: Int, + ) { + val sheet = repository.characterSheetFlow(id = argument.id).value + val updated = when (characteristicId) { + CharacterSheetFactory.HP -> sheet?.copy(currentHp = max(0, min(sheet.maxHp, value))) + CharacterSheetFactory.PP -> sheet?.copy(currentPP = max(0, min(sheet.maxPP, value))) + else -> null + } + updated?.let { + repository.save(it) + } + } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/detail/CharacterSheetDeleteConfirmationDialog.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/detail/dialog/CharacterSheetDeleteConfirmationDialog.kt similarity index 98% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/detail/CharacterSheetDeleteConfirmationDialog.kt rename to composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/detail/dialog/CharacterSheetDeleteConfirmationDialog.kt index 1cdefa3..acf770d 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/detail/CharacterSheetDeleteConfirmationDialog.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/detail/dialog/CharacterSheetDeleteConfirmationDialog.kt @@ -1,4 +1,4 @@ -package com.pixelized.desktop.lwa.screen.characterSheet.detail +package com.pixelized.desktop.lwa.screen.characterSheet.detail.dialog import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/detail/dialog/CharacterSheetStatDialog.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/detail/dialog/CharacterSheetStatDialog.kt new file mode 100644 index 0000000..b99877e --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/detail/dialog/CharacterSheetStatDialog.kt @@ -0,0 +1,158 @@ +package com.pixelized.desktop.lwa.screen.characterSheet.detail.dialog + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.SizeTransform +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.background +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.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable +import androidx.compose.runtime.State +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp + +private val DefaultScrimColor = Color.Black.copy(alpha = 0.6f) + +@Stable +data class StatChangeDialogUio( + val id: String, + val value: () -> TextFieldValue, + val placeholder: String, + val onValueChange: (TextFieldValue) -> Unit, + val maxValue: String, +) + +@Composable +fun CharacterSheetStatDialog( + dialog: State, + onConfirm: (StatChangeDialogUio) -> Unit, + onDismissRequest: () -> Unit, +) { + AnimatedVisibility( + visible = dialog.value != null, + ) { + Box( + modifier = Modifier + .fillMaxSize() + .background(color = DefaultScrimColor), + ) + } + AnimatedContent( + modifier = Modifier.fillMaxSize(), + targetState = dialog.value, + transitionSpec = { + val enter = fadeIn() + slideInVertically { 32 } + val exit = fadeOut() + slideOutVertically { 32 } + enter togetherWith exit using SizeTransform(clip = false) + }, + ) { + Box( + modifier = Modifier.fillMaxSize() + ) { + when (it) { + null -> Box( + modifier = Modifier, + ) + + else -> Dialog( + dialog = it, + onConfirm = onConfirm, + onDismissRequest = onDismissRequest, + ) + } + } + } +} + +@Composable +private fun Dialog( + dialog: StatChangeDialogUio, + onConfirm: (StatChangeDialogUio) -> Unit, + onDismissRequest: () -> Unit, +) { + val typography = MaterialTheme.typography + val colors = MaterialTheme.colors + + val focusRequester = remember { FocusRequester() } + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } + + Box( + modifier = Modifier + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = onDismissRequest, + ) + .fillMaxSize() + .padding(all = 32.dp), + contentAlignment = Alignment.Center, + ) { + Surface( + shape = remember { RoundedCornerShape(size = 16.dp) }, + ) { + Row( + modifier = Modifier.padding(all = 16.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + TextField( + modifier = Modifier + .focusRequester(focusRequester = focusRequester) + .alignByBaseline() + .width(width = 120.dp), + value = dialog.value(), + textStyle = remember { + typography.body1.copy( + color = colors.primary, + textAlign = TextAlign.End, + fontWeight = FontWeight.Bold + ) + }, + singleLine = true, + keyboardActions = KeyboardActions { onConfirm(dialog) }, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + placeholder = { dialog.placeholder }, + onValueChange = dialog.onValueChange, + ) + Text( + modifier = Modifier.alignByBaseline(), + text = "/", + ) + Text( + modifier = Modifier.alignByBaseline(), + text = dialog.maxValue, + ) + } + } + } +} \ No newline at end of file