From d74a5fcd7cba750b6caa913c1a4ca6e611a102bb Mon Sep 17 00:00:00 2001 From: Thomas Andres Gomez Date: Sun, 3 Nov 2024 16:02:29 +0100 Subject: [PATCH] Basic character sheet edit. --- .../kotlin/com/pixelized/desktop/lwa/App.kt | 76 ++++- .../characterSheet/edit/CharacterSheetEdit.kt | 323 ++++++++++++++++++ .../screen/characterSheet/edit/FieldUio.kt | 104 ++++++ 3 files changed, 501 insertions(+), 2 deletions(-) create mode 100644 composeApp/src/desktopMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/edit/CharacterSheetEdit.kt create mode 100644 composeApp/src/desktopMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/edit/FieldUio.kt diff --git a/composeApp/src/desktopMain/kotlin/com/pixelized/desktop/lwa/App.kt b/composeApp/src/desktopMain/kotlin/com/pixelized/desktop/lwa/App.kt index 3abdeb6..af8decf 100644 --- a/composeApp/src/desktopMain/kotlin/com/pixelized/desktop/lwa/App.kt +++ b/composeApp/src/desktopMain/kotlin/com/pixelized/desktop/lwa/App.kt @@ -1,20 +1,31 @@ package com.pixelized.desktop.lwa 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.material.Button +import androidx.compose.material.Icon +import androidx.compose.material.IconButton import androidx.compose.material.Surface import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Edit import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Window import androidx.compose.ui.window.rememberWindowState import androidx.lifecycle.viewmodel.compose.viewModel import com.pixelized.desktop.lwa.screen.characterSheet.CharacterSheet +import com.pixelized.desktop.lwa.screen.characterSheet.CharacterSheetUio import com.pixelized.desktop.lwa.screen.characterSheet.CharacterSheetViewModel +import com.pixelized.desktop.lwa.screen.characterSheet.edit.CharacterSheetEdit +import com.pixelized.desktop.lwa.screen.characterSheet.edit.CharacterSheetEditUio +import com.pixelized.desktop.lwa.screen.characterSheet.edit.FieldUio import com.pixelized.desktop.lwa.screen.overlay.BlurOverlay import com.pixelized.desktop.lwa.screen.overlay.BlurOverlayViewModel import com.pixelized.desktop.lwa.screen.roll.RollPage @@ -33,13 +44,34 @@ fun App() { val overlayViewModel = viewModel { BlurOverlayViewModel() } val rollViewModel = viewModel { RollViewModel() } + val edit = remember { mutableStateOf(null) } + Column( modifier = Modifier.padding(all = 16.dp), ) { + Row { + Button( + onClick = sheetViewModel::showCharacterSheet, + ) { + Text(text = "Koryas Tissenpa") + } + IconButton( + onClick = { + edit.value = CharacterSheetEditUio.create( + sheet = CharacterSheetUio.Koryas, + ) + } + ) { + Icon( + imageVector = Icons.Default.Edit, + contentDescription = null + ) + } + } Button( - onClick = sheetViewModel::showCharacterSheet, + onClick = { edit.value = CharacterSheetEditUio.Default }, ) { - Text(text = "Koryas Tissenpa") + Text(text = "Créer une feuille de personnage") } } @@ -82,6 +114,46 @@ fun App() { } } } + + edit.value?.let { sheet -> + Window( + onCloseRequest = { edit.value = null }, + state = rememberWindowState( + width = 320.dp + 64.dp, + height = 900.dp, + ), + title = "LwaCharacterSheet", + ) { + Surface( + modifier = Modifier.fillMaxSize(), + ) { + CharacterSheetEdit( + form = sheet, + onSkill = { skill -> + edit.value = sheet.copy( + groups = sheet.groups.map { group -> + if (skill.title == group.title) { + group.copy( + fields = mutableListOf().apply { + addAll(group.fields) + add( + FieldUio.create( + label = "", + valuePlaceHolder = { "40" }, + ) + ) + } + ) + } else { + group + } + } + ) + } + ) + } + } + } } } } \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/edit/CharacterSheetEdit.kt b/composeApp/src/desktopMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/edit/CharacterSheetEdit.kt new file mode 100644 index 0000000..287e87c --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/edit/CharacterSheetEdit.kt @@ -0,0 +1,323 @@ +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.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.pixelized.desktop.lwa.composable.DecoratedBox +import com.pixelized.desktop.lwa.screen.characterSheet.CharacterSheetUio +import kotlin.math.max +import kotlin.math.truncate + +@Stable +data class CharacterSheetEditUio( + val name: FieldUio, + val groups: List, +) { + @Stable + data class Group( + val title: String, + val editable: Boolean = false, + val fields: List, + ) + + companion object { + val Default = run { + val strField = FieldUio.create(label = "Force", valuePlaceHolder = { "0" }) + fun str(): Int = strField.value.value.toIntOrNull() ?: 0 + val dexField = FieldUio.create(label = "Dextérité", valuePlaceHolder = { "0" }) + fun dex(): Int = dexField.value.value.toIntOrNull() ?: 0 + val conField = FieldUio.create(label = "Constitution", valuePlaceHolder = { "0" }) + fun con(): Int = conField.value.value.toIntOrNull() ?: 0 + val vitField = FieldUio.create(label = "Taille", valuePlaceHolder = { "0" }) + fun vit(): Int = vitField.value.value.toIntOrNull() ?: 0 + val intField = FieldUio.create(label = "Intelligence", valuePlaceHolder = { "0" }) + fun int(): Int = intField.value.value.toIntOrNull() ?: 0 + val powField = FieldUio.create(label = "Pouvoir", valuePlaceHolder = { "0" }) + fun pow(): Int = powField.value.value.toIntOrNull() ?: 0 + val chaField = FieldUio.create(label = "Charisme", valuePlaceHolder = { "0" }) + fun cha(): Int = chaField.value.value.toIntOrNull() ?: 0 + + CharacterSheetEditUio( + name = FieldUio.create( + useLabelAsPlaceholder = true, + label = "Name", + ), + groups = listOf( + Group( + title = "Charactéristiques", + fields = listOf( + strField, + dexField, + conField, + vitField, + intField, + powField, + chaField + ), + ), + Group( + title = "Charactéristiques dérivées", + fields = listOf( + FieldUio.create( + label = "Déplacement", + valuePlaceHolder = { "10" }, + ), + FieldUio.create( + label = "Points de vie", + valuePlaceHolder = { "${(con() + vit()) / 2}" }, + ), + FieldUio.create( + label = "Points de pouvoir", + valuePlaceHolder = { "${pow()}" }, + ), + FieldUio.create( + label = "Bonus aux dégats", + valuePlaceHolder = { + val bonus = str() + vit() + 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" + } + }, + ), + FieldUio.create(label = "Armure", valuePlaceHolder = { "0" }), + ), + ), + Group( + title = "Compétances", + editable = true, + fields = listOf( + FieldUio.create( + label = "Bagarre", + valuePlaceHolder = { trunc(dex() * 2) }, + ), + FieldUio.create( + label = "Esquive", + valuePlaceHolder = { trunc(dex() * 2) } + ), + FieldUio.create( + label = "Saisie", + valuePlaceHolder = { trunc(str() + vit()) }, + ), + FieldUio.create( + label = "Lancer", + valuePlaceHolder = { trunc(str() + dex()) }, + ), + FieldUio.create( + label = "Athlétisme", + valuePlaceHolder = { trunc(str() + con() * 2) }, + ), + FieldUio.create( + label = "Acrobatie", + valuePlaceHolder = { trunc(dex() + con() * 2) }, + ), + FieldUio.create( + label = "Perception", + valuePlaceHolder = { trunc(10 + int() * 2) }, + ), + FieldUio.create( + label = "Recherche", + valuePlaceHolder = { trunc(10 + int() * 2) }, + ), + FieldUio.create( + label = "Empathie", + valuePlaceHolder = { trunc(cha() + int()) }, + ), + FieldUio.create( + label = "Persuasion", + valuePlaceHolder = { trunc(cha() * 3) }, + ), + FieldUio.create( + label = "Intimidation", + valuePlaceHolder = { trunc(cha() + max(pow(), vit()) * 2) }, + ), + FieldUio.create( + label = "Baratin", + valuePlaceHolder = { trunc(cha() * 2 + int()) }, + ), + FieldUio.create( + label = "Marchandage", + valuePlaceHolder = { trunc(cha() * 2) }, + ), + FieldUio.create( + label = "Discrétion", + valuePlaceHolder = { trunc(cha() + dex() * 2 - vit()) }, + ), + FieldUio.create( + label = "Escamotage", + valuePlaceHolder = { trunc(dex() * 2) }, + ), + FieldUio.create( + label = "Premiers soins", + valuePlaceHolder = { trunc(int() + dex()) }, + ), + ), + ), + Group( + title = "Occupations", + editable = true, + fields = emptyList(), + ), + Group( + title = "Compétences magiques", + editable = true, + fields = emptyList(), + ), + ) + ) + } + + fun create(sheet: CharacterSheetUio): CharacterSheetEditUio { + return CharacterSheetEditUio( + name = FieldUio.create( + useLabelAsPlaceholder = true, + label = "Name", + initialValue = sheet.name, + ), + groups = listOf( + Group( + title = "Charactéristiques", + fields = sheet.characteristics.map { + FieldUio.create( + label = it.label, + initialValue = it.value, + ) + }, + ), + Group( + title = "Charactéristiques dérivées", + fields = sheet.subCharacteristics.map { + FieldUio.create( + label = it.label, + initialValue = it.value, + ) + }, + ), + Group( + title = "Compétances", + fields = sheet.skills.map { + FieldUio.create( + label = it.label, + initialValue = "${it.value}", + ) + }, + ), + Group( + title = "Occupations", + editable = true, + fields = sheet.occupations.map { + FieldUio.create( + label = it.label, + initialValue = "${it.value}", + ) + }, + ), + Group( + title = "Compétences magiques", + editable = true, + fields = sheet.magics.map { + FieldUio.create( + label = it.label, + initialValue = "${it.value}", + ) + }, + ), + ) + ) + } + } +} + +private fun trunc(value: Int): String { + return "${(truncate(value.toFloat() / 5f) * 5f).toInt()}" +} + +@Composable +fun CharacterSheetEdit( + form: CharacterSheetEditUio, + onSkill: (CharacterSheetEditUio.Group) -> Unit, +) { + Column( + modifier = Modifier + .verticalScroll(state = rememberScrollState()) + .padding(all = 24.dp), + verticalArrangement = Arrangement.spacedBy(space = 16.dp) + ) { + Form( + modifier = Modifier.fillMaxWidth(), + field = form.name, + ) + + form.groups.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 + ) + ) { + Button( + colors = ButtonDefaults.textButtonColors(), + onClick = { onSkill(it) }, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(space = 4.dp), + ) { + Text( + style = MaterialTheme.typography.caption, + text = "Ajouter une ligne", + ) + Icon( + imageVector = Icons.Default.Add, + contentDescription = null, + ) + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/edit/FieldUio.kt b/composeApp/src/desktopMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/edit/FieldUio.kt new file mode 100644 index 0000000..7a96de6 --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/edit/FieldUio.kt @@ -0,0 +1,104 @@ +package com.pixelized.desktop.lwa.screen.characterSheet.edit + +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.width +import androidx.compose.foundation.text.KeyboardActions +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.unit.dp + +@Stable +open class FieldUio( + val useLabelAsPlaceholder: Boolean, + val label: State, + val onLabelChange: (String) -> Unit, + val valuePlaceHolder: State, + val value: State, + val onValueChange: (String) -> Unit, +) { + companion object { + @Stable + fun create( + useLabelAsPlaceholder: Boolean = false, + label: String = "", + initialValue: String = "", + valuePlaceHolder: () -> String = { "" }, + ): FieldUio { + val labelState = mutableStateOf(label) + val valueState = mutableStateOf(initialValue) + return FieldUio( + useLabelAsPlaceholder = useLabelAsPlaceholder, + 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, + ) { + TextField( + modifier = Modifier.weight(weight = 1f), + value = field.label.value, + placeholder = { Text(text = "Nom") }, + singleLine = true, + keyboardActions = KeyboardActions { focus.moveFocus(FocusDirection.Next) }, + onValueChange = field.onLabelChange, + ) + 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, + ) + } + } + } + } +} \ No newline at end of file