diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index f01e0d7..846ddb6 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -3,7 +3,6 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat plugins { alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.kotlinSerialization) - // alias(libs.plugins.kotlinKtor) alias(libs.plugins.composeMultiplatform) alias(libs.plugins.composeCompiler) } @@ -22,6 +21,7 @@ kotlin { implementation(compose.ui) implementation(compose.components.resources) implementation(compose.components.uiToolingPreview) + implementation(libs.compose.desktop.preview) implementation(libs.androidx.lifecycle.viewmodel) implementation(libs.androidx.lifecycle.runtime.compose) implementation(libs.androidx.navigation.compose) diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml index 7763cdc..7e0e41a 100644 --- a/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -19,7 +19,7 @@ Création de personnage Nom - Ajouter un lancer + Ajouter une action Sauvegarder Caractéristiques Force @@ -35,8 +35,15 @@ Points de pouvoir Bonus aux dégats Armure + Compétences communes + Compétences spéciales + Ajouter une compétence spéciale + Compétences magiques + Ajouter une compétence magique + Base + Bonus + Niveau Compétences - Ajouter une compétence Bagarre Esquive Saisie @@ -57,6 +64,9 @@ Ajouter une occupation Compétences magiques Ajouter une compétence magique + Supprimer + Compétence d'occupation + État diminuer Modifier @@ -74,9 +84,9 @@ Points de pouvoir Bonus aux dégats Armure - Compétences - Occupations - Compétences magiques + Compétences communes + Compétences spéciales + Compétences magiques Supprimer la feuille de personnage Êtes-vous sûr de vouloir supprimer "%1$s" ? Confirmer @@ -93,7 +103,7 @@ Le Déplacement (DEP) est une valeur de jeu qui détermine la distance que peut parcourir un personnage en un round de combat. Tous les humains ont un DEP de 10. Le DEP a une valeur réelle flexible, mais généralement, chaque point de DEP équivaut à un déplacement d’un mètre. En course, un point équivaut à trois mètres. Les points de vie (PV) sont égaux à la somme CON+TAI du personnage, divisée par deux (arrondie au supérieur). Ils sont soustraits lorsque le personnage subit des dommages. Quand les points de vie tombent à 0, le personnage sombre dans l’inconscience. S'il reste inconscient trop longtemps, il meurt. Tous les points de vie régénèrent naturellement après une nuit de repos. les points de pouvoir sont égaux au POU et sont dépensés pour utiliser la magie ou d’autres pouvoirs. Tous les points de pouvoir régénèrent naturellement après une nuit de repos. - Les personnages plus massifs ou plus forts infligent plus de dégâts quand ils frappent leurs ennemis en combat au corps à corps. Le modificateur s’applique aux dégâts infligés par toute attaque portée par les personnages avec des armes de mêlée. La moitié de ce bonus s'applique aux attaques de lancer. + Les personnages plus massifs ou plus forts infligent plus de dégâts quand ils frappent leurs ennemis en combat au corps à corps. Le modificateur s’applique aux dégâts infligés par toute attaque portée par les personnages avec des armes de mêlée (BDC). La moitié de ce bonus s'applique aux attaques de lancer (BDD). Une armure protège son porteur des blessures. Lorsqu’un personnage est touché en combat par une attaque non magique, soustrayez les points d’armure aux points de dégâts infligés. Les dommages au-delà de la protection de l’armure surpassent celle-ci et sont infligés au personnage, réduisant ses points de vie actuels. Attaque en combat à mains nues. Une attaque réussie inflige 1D3 + BDGT. diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/Module.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/Module.kt index 3425dcc..2f6a53f 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/Module.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/Module.kt @@ -1,10 +1,14 @@ package com.pixelized.desktop.lwa +import com.pixelized.desktop.lwa.business.DamageBonusUseCase +import com.pixelized.desktop.lwa.business.SkillValueComputationUseCase +import com.pixelized.desktop.lwa.parser.arithmetic.ArithmeticParser import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetJsonFactory import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetStore import com.pixelized.desktop.lwa.repository.network.NetworkRepository import com.pixelized.desktop.lwa.repository.roll.RollHistoryRepository +import com.pixelized.desktop.lwa.screen.characterSheet.common.SkillFieldFactory import com.pixelized.desktop.lwa.screen.characterSheet.detail.CharacterSheetFactory import com.pixelized.desktop.lwa.screen.characterSheet.detail.CharacterSheetViewModel import com.pixelized.desktop.lwa.screen.characterSheet.edit.CharacterSheetEditFactory @@ -21,9 +25,11 @@ import org.koin.dsl.module val moduleDependencies get() = listOf( - repositoryDependencies, + parserDependencies, factoryDependencies, + repositoryDependencies, viewModelDependencies, + useCaseDependencies, ) val repositoryDependencies @@ -40,6 +46,7 @@ val factoryDependencies factoryOf(::CharacterSheetEditFactory) factoryOf(::CharacterSheetJsonFactory) factoryOf(::NetworkFactory) + factoryOf(::SkillFieldFactory) } val viewModelDependencies @@ -50,4 +57,15 @@ val viewModelDependencies viewModelOf(::RollViewModel) viewModelOf(::RollHistoryViewModel) viewModelOf(::NetworkViewModel) - } \ No newline at end of file + } + +val parserDependencies + get() = module { + factoryOf(::ArithmeticParser) + } + +val useCaseDependencies + get() = module { + factoryOf(::DamageBonusUseCase) + factoryOf(::SkillValueComputationUseCase) + } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/business/DamageBonusUseCase.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/business/DamageBonusUseCase.kt index 553aa76..5aeef35 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/business/DamageBonusUseCase.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/business/DamageBonusUseCase.kt @@ -1,6 +1,6 @@ package com.pixelized.desktop.lwa.business -object DamageBonusUseCase { +class DamageBonusUseCase { fun bonusDamage(strength: Int, height: Int): String { return bonusDamage(stat = strength + height) diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/business/SkillValueComputationUseCase.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/business/SkillValueComputationUseCase.kt new file mode 100644 index 0000000..fc762a2 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/business/SkillValueComputationUseCase.kt @@ -0,0 +1,40 @@ +package com.pixelized.desktop.lwa.business + +import com.pixelized.desktop.lwa.parser.arithmetic.Instruction +import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheet +import kotlin.math.max + +class SkillValueComputationUseCase { + + fun computeSkillValue( + sheet: CharacterSheet, + skill: CharacterSheet.Skill, + diminished: Int, + ): Int { + val baseSum = skill.base.sumOf { + when (val instruction = it.instruction) { + is Instruction.Dice -> 0 + is Instruction.Flat -> instruction.value + Instruction.Word.BDC -> 0 + Instruction.Word.BDD -> 0 + Instruction.Word.STR -> sheet.strength + Instruction.Word.DEX -> sheet.dexterity + Instruction.Word.CON -> sheet.constitution + Instruction.Word.HEI -> sheet.height + Instruction.Word.INT -> sheet.intelligence + Instruction.Word.POW -> sheet.power + Instruction.Word.CHA -> sheet.charisma + } * it.sign + } + val base = if (skill.occupation) { + max(MIN_OCCUPATION_VALUE, baseSum) + } else { + baseSum + } + return max(base + skill.bonus + skill.level - diminished, 0) + } + + companion object { + private const val MIN_OCCUPATION_VALUE = 40 + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/composable/decoratedBox/DecoratedBox.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/composable/decoratedBox/DecoratedBox.kt index 69f18e7..0867d42 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/composable/decoratedBox/DecoratedBox.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/composable/decoratedBox/DecoratedBox.kt @@ -1,19 +1,21 @@ package com.pixelized.desktop.lwa.composable.decoratedBox +import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width 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.Alignment 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 +import com.pixelized.desktop.lwa.utils.preview.ContentPreview @Composable fun DecoratedBox( @@ -34,12 +36,14 @@ fun DecoratedBox( @Composable @Preview private fun DecoratedBoxPreview() { - LwaTheme { - Surface { - DecoratedBox { - Text("test") + ContentPreview { + DecoratedBox { + Box( + modifier = Modifier.width(width = 128.dp).height(64.dp), + contentAlignment = Alignment.Center, + ) { + Text("Test") } } } -} - +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/navigation/window/WindowNavHost.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/navigation/window/WindowNavHost.kt index e709679..66e221e 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/navigation/window/WindowNavHost.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/navigation/window/WindowNavHost.kt @@ -7,7 +7,6 @@ import androidx.compose.runtime.State import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.window.Window -import androidx.compose.ui.window.rememberWindowState import com.pixelized.desktop.lwa.navigation.window.destination.Window val LocalWindow = compositionLocalOf { @@ -43,10 +42,7 @@ fun WindowsNavHost( ) { Window( onCloseRequest = { controller.hideWindow(id = window.id) }, - state = rememberWindowState( - width = window.width, - height = window.height, - ), + state = window.state, title = window.title, ) { content.invoke(window) diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/navigation/window/destination/CharacterSheetCreateWindow.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/navigation/window/destination/CharacterSheetCreateWindow.kt index ff5e966..fa5ef18 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/navigation/window/destination/CharacterSheetCreateWindow.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/navigation/window/destination/CharacterSheetCreateWindow.kt @@ -1,6 +1,15 @@ package com.pixelized.desktop.lwa.navigation.window.destination import androidx.compose.runtime.Stable +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp @Stable -class CharacterSheetCreateWindow : Window(title = "") \ No newline at end of file +class CharacterSheetCreateWindow : Window( + title = "", + size = size, +) { + companion object { + val size = DpSize(600.dp, 900.dp) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/navigation/window/destination/CharacterSheetWindow.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/navigation/window/destination/CharacterSheetWindow.kt index ee7009b..e841a92 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/navigation/window/destination/CharacterSheetWindow.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/navigation/window/destination/CharacterSheetWindow.kt @@ -1,6 +1,8 @@ package com.pixelized.desktop.lwa.navigation.window.destination import androidx.compose.runtime.Stable +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp @Stable class CharacterSheetWindow( @@ -8,5 +10,13 @@ class CharacterSheetWindow( characterName: String, ) : Window( title = characterName, -) + size = size, +) { + companion object { + val size = DpSize( + width = 400.dp + 64.dp, + height = 900.dp, + ) + } +} diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/navigation/window/destination/Window.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/navigation/window/destination/Window.kt index c3412bb..eea81b4 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/navigation/window/destination/Window.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/navigation/window/destination/Window.kt @@ -2,13 +2,31 @@ package com.pixelized.desktop.lwa.navigation.window.destination import androidx.compose.runtime.Stable import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.window.WindowPlacement +import androidx.compose.ui.window.WindowPosition +import androidx.compose.ui.window.WindowState import java.util.UUID @Stable sealed class Window( val id: String = UUID.randomUUID().toString(), val title: String, - val width: Dp = 400.dp + 64.dp, - val height: Dp = 900.dp, -) \ No newline at end of file + size: DpSize, +) { + val state = WindowState( + placement = WindowPlacement.Floating, + isMinimized = false, + position = WindowPosition.PlatformDefault, + width = size.width, + height = size.height, + ) + + val width: Dp get() = state.size.width + val height: Dp get() = state.size.height + val size: DpSize get() = state.size + + suspend fun resize(size: DpSize) { + state.size = size + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/parser/arithmetic/ArithmeticParser.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/parser/arithmetic/ArithmeticParser.kt new file mode 100644 index 0000000..1bac89c --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/parser/arithmetic/ArithmeticParser.kt @@ -0,0 +1,112 @@ +package com.pixelized.desktop.lwa.parser.arithmetic + +import com.pixelized.desktop.lwa.parser.arithmetic.Instruction.Dice.Modifier +import com.pixelized.desktop.lwa.parser.arithmetic.Instruction.Word + +// https://medium.com/@gustavoinzunza/how-to-solve-balanced-parenthesis-problem-with-regex-in-ruby-113eff30a79a +// https://stackoverflow.com/questions/546433/regular-expression-to-match-balanced-parentheses +class ArithmeticParser { + + private val operatorParser = Regex( + """\s*(?[-+])?\s*(?[^-+]*)""" + ) + private val diceParser = Regex( + """^(?[ade])?(?\d+)[dD](?\d+)""" + ) + private val wordParser = Regex( + """^(?${words.joinToString(separator = "|") { it }})""" + ) + private val flatParser = Regex( + """(?\d+)""" + ) + + @Throws(Instruction.UnknownInstruction::class) + fun parse(value: String): List { + return operatorParser.findAll(value).mapNotNull { + val (operator, instruction) = it.destructured + if (instruction.isNotBlank()) { + Arithmetic( + sign = parseOperator(operator), + instruction = parseInstruction(instruction), + ) + } else { + null + } + }.toList() + } + + @Throws(Instruction.UnknownInstruction::class) + fun parseInstruction(instruction: String): Instruction { + diceParser.find(instruction)?.let { + val (modifier, quantity, faces) = it.destructured + Instruction.Dice( + modifier = parseModifier(value = modifier), + quantity = quantity.toInt(), + faces = faces.toInt(), + ) + }?.let { return it } + + wordParser.find(instruction)?.let { + parseWord(value = it.value) + }?.let { return it } + + flatParser.find(instruction)?.let { + Instruction.Flat(value = it.value.toInt()) + }?.let { return it } + + throw Instruction.UnknownInstruction(payload = instruction) + } + + private fun parseOperator(operator: String): Int { + return when (operator) { + "-" -> -1 + "+" -> 1 + else -> 1 + } + } + + private fun parseModifier(value: String): Modifier? { + return when (value) { + "a" -> Modifier.ADVANTAGE + "d" -> Modifier.DISADVANTAGE + "e" -> Modifier.EMPHASIS + else -> null + } + } + + private fun parseWord(value: String): Word? { + return try { + Word.valueOf(value) + } catch (_: Exception) { + return null + } + } + + fun convertInstructionToString( + instructions: List, + ): String { + return instructions.map { + val sign = if (it.sign > 0) "+" else "-" + val value = when (val instruction = it.instruction) { + is Instruction.Dice -> "${instruction.modifier ?: ""}${instruction.quantity}d${instruction.faces}" + is Instruction.Flat -> "${instruction.value}" + Word.BDC -> Word.BDC.name + Word.BDD -> Word.BDD.name + Word.STR -> Word.STR.name + Word.DEX -> Word.DEX.name + Word.CON -> Word.CON.name + Word.HEI -> Word.HEI.name + Word.INT -> Word.INT.name + Word.POW -> Word.POW.name + Word.CHA -> Word.CHA.name + } + "$sign$value" + }.joinToString(separator = " ") { + it + } + } + + companion object { + val words: List = Word.entries.map { it.name } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/parser/arithmetic/Instruction.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/parser/arithmetic/Instruction.kt new file mode 100644 index 0000000..8d254d4 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/parser/arithmetic/Instruction.kt @@ -0,0 +1,40 @@ +package com.pixelized.desktop.lwa.parser.arithmetic + +class Arithmetic( + val sign: Int, + val instruction: Instruction, +) + +sealed interface Instruction { + data class Dice( + val modifier: Modifier?, + val quantity: Int, + val faces: Int, + ) : Instruction { + enum class Modifier { + ADVANTAGE, + DISADVANTAGE, + EMPHASIS, + } + } + + data class Flat( + val value: Int, + ) : Instruction + + enum class Word : Instruction { + BDC, // Damages bonus for melee + BDD, // Damages bonus for range + STR, // Strength + DEX, // Dexterity + CON, // Constitution + HEI, // Height + INT, // Intelligence + POW, // Power + CHA, // Charisma + } + + class UnknownInstruction(payload: String) : RuntimeException( + "Unknown instruction exception. Unable to parse the following payload:\"$payload\" into an instruction" + ) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetJsonFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetJsonFactory.kt index ffb2468..870156b 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetJsonFactory.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetJsonFactory.kt @@ -1,12 +1,18 @@ package com.pixelized.desktop.lwa.repository.characterSheet +import com.pixelized.desktop.lwa.business.DamageBonusUseCase +import com.pixelized.desktop.lwa.parser.arithmetic.ArithmeticParser import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheet import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheetJson import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheetJsonV1 +import kotlin.math.ceil -class CharacterSheetJsonFactory { +class CharacterSheetJsonFactory( + private val bonusDamageUseCase: DamageBonusUseCase, + private val arithmeticParser: ArithmeticParser, +) { - fun convertLastJsonVersion( + fun convertToJson( sheet: CharacterSheet, ): CharacterSheetJson { val json = CharacterSheetJsonV1( @@ -19,38 +25,51 @@ class CharacterSheetJsonFactory { intelligence = sheet.intelligence, power = sheet.power, charisma = sheet.charisma, - movement = sheet.movement, + movement = if (sheet.overrideMovement) sheet.movement else null, currentHp = sheet.currentHp, - maxHp = sheet.maxHp, - currentPP = sheet.currentPP, - maxPP = sheet.maxPP, - damageBonus = sheet.damageBonus, - armor = sheet.armor, - skills = sheet.skills.map { + maxHp = if (sheet.overrideMaxHp) sheet.maxHp else null, + currentPP = sheet.currentPp, + maxPP = if (sheet.overrideMaxPP) sheet.maxPP else null, + damageBonus = if (sheet.overrideDamageBonus) sheet.damageBonus else null, + armor = if (sheet.overrideArmor) sheet.armor else null, + skills = sheet.commonSkills.map { CharacterSheetJsonV1.Skill( + id = it.id, label = it.label, - value = it.value, + base = arithmeticParser.convertInstructionToString(it.base), + bonus = it.bonus, + level = it.level, + occupation = it.occupation, used = it.used, ) }, - occupations = sheet.occupations.map { + occupations = sheet.specialSkills.map { CharacterSheetJsonV1.Skill( + id = it.id, label = it.label, - value = it.value, + base = arithmeticParser.convertInstructionToString(it.base), + bonus = it.bonus, + level = it.level, + occupation = it.occupation, used = it.used, ) }, - magics = sheet.magics.map { + magics = sheet.magicSkills.map { CharacterSheetJsonV1.Skill( + id = it.id, label = it.label, - value = it.value, + base = arithmeticParser.convertInstructionToString(it.base), + bonus = it.bonus, + level = it.level, + occupation = it.occupation, used = it.used, ) }, - rolls = sheet.rolls.map { + rolls = sheet.actions.map { CharacterSheetJsonV1.Roll( + id = it.id, label = it.label, - roll = it.roll, + roll = arithmeticParser.convertInstructionToString(it.roll), ) }, ) @@ -78,38 +97,60 @@ class CharacterSheetJsonFactory { intelligence = json.intelligence, power = json.power, charisma = json.charisma, - movement = json.movement, + overrideMovement = json.movement != null, + movement = json.movement ?: 10, currentHp = json.currentHp, - maxHp = json.maxHp, - currentPP = json.currentPP, - maxPP = json.maxPP, - damageBonus = json.damageBonus, - armor = json.armor, - skills = json.skills.map { + overrideMaxHp = json.maxHp != null, + maxHp = json.maxHp ?: (ceil((json.constitution + json.height) / 2f).toInt()), + currentPp = json.currentPP, + overrideMaxPP = json.maxPP != null, + maxPP = json.maxPP ?: json.power, + overrideDamageBonus = json.damageBonus != null, + damageBonus = json.damageBonus + ?: bonusDamageUseCase.bonusDamage( + strength = json.strength, + height = json.height + ), + overrideArmor = json.armor != null, + armor = json.armor ?: 0, + commonSkills = json.skills.map { CharacterSheet.Skill( + id = it.id, label = it.label, - value = it.value, + base = arithmeticParser.parse(it.base), + bonus = it.bonus, + level = it.level, + occupation = it.occupation, used = it.used, ) }, - occupations = json.occupations.map { + specialSkills = json.occupations.map { CharacterSheet.Skill( + id = it.id, label = it.label, - value = it.value, + base = arithmeticParser.parse(it.base), + bonus = it.bonus, + level = it.level, + occupation = it.occupation, used = it.used, ) }, - magics = json.magics.map { + magicSkills = json.magics.map { CharacterSheet.Skill( + id = it.id, label = it.label, - value = it.value, + base = arithmeticParser.parse(it.base), + bonus = it.bonus, + level = it.level, + occupation = it.occupation, used = it.used, ) }, - rolls = json.rolls.map { + actions = json.rolls.map { CharacterSheet.Roll( + id = it.id, label = it.label, - roll = it.roll, + roll = arithmeticParser.parse(it.roll), ) }, ) diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetStore.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetStore.kt index df50663..29b9856 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetStore.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetStore.kt @@ -26,7 +26,7 @@ class CharacterSheetStore( fun save(sheets: List) { val json = try { sheets - .map(factory::convertLastJsonVersion) + .map(factory::convertToJson) .let(Json::encodeToString) } catch (exception: Exception) { throw JsonConversionException(root = exception) diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/model/CharacterSheet.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/model/CharacterSheet.kt index 34fe597..47a2473 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/model/CharacterSheet.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/model/CharacterSheet.kt @@ -1,5 +1,7 @@ package com.pixelized.desktop.lwa.repository.characterSheet.model +import com.pixelized.desktop.lwa.parser.arithmetic.Arithmetic + data class CharacterSheet( val id: String, val name: String, @@ -12,49 +14,57 @@ data class CharacterSheet( val power: Int, val charisma: Int, // sub characteristics + val overrideMovement: Boolean, val movement: Int, val currentHp: Int, + val overrideMaxHp: Boolean, val maxHp: Int, - val currentPP: Int, + val currentPp: Int, + val overrideMaxPP: Boolean, val maxPP: Int, + val overrideDamageBonus: Boolean, val damageBonus: String, + val overrideArmor: Boolean, val armor: Int, // skills - val skills: List, - // occupations - val occupations: List, - // magic skill - val magics: List, - // attack - val rolls: List, + val commonSkills: List, + val specialSkills: List, + val magicSkills: List, + // actions + val actions: List, ) { data class Skill( + val id: String, val label: String, - val value: Int, + val base: List, + val bonus: Int, + val level: Int, + val occupation: Boolean, val used: Boolean, ) data class Roll( + val id: String, val label: String, - val roll: String, + val roll: List, ) - companion object { - const val COMBAT = "Bagarre" - const val DODGE = "Esquive" - const val GRAB = "Saisie" - const val THROW = "Lancer" - const val ATHLETICS = "Athlétisme" - const val ACROBATICS = "Acrobatie" - const val PERCEPTION = "Perception" - const val SEARCH = "Recherche" - const val EMPATHY = "Empathie" - const val PERSUASION = "Persuasion" - const val INTIMIDATION = "Intimidation" - const val SPIEL = "Baratin" - const val BARGAIN = "Marchandage" - const val DISCRETION = "Discrétion" - const val SLEIGHT_OF_HAND = "Escamotage" - const val AID = "Premiers soins" + object CommonSkillId { + const val COMBAT_ID = "Bagarre" + const val DODGE_ID = "Esquive" + const val GRAB_ID = "Saisie" + const val THROW_ID = "Lancer" + const val ATHLETICS_ID = "Athlétisme" + const val ACROBATICS_ID = "Acrobatie" + const val PERCEPTION_ID = "Perception" + const val SEARCH_ID = "Recherche" + const val EMPATHY_ID = "Empathie" + const val PERSUASION_ID = "Persuasion" + const val INTIMIDATION_ID = "Intimidation" + const val SPIEL_ID = "Baratin" + const val BARGAIN_ID = "Marchandage" + const val DISCRETION_ID = "Discrétion" + const val SLEIGHT_OF_HAND_ID = "Escamotage" + const val AID_ID = "Premiers soins" } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/model/CharacterSheetJsonV1.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/model/CharacterSheetJsonV1.kt index a342f39..3219c2e 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/model/CharacterSheetJsonV1.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/model/CharacterSheetJsonV1.kt @@ -15,13 +15,13 @@ data class CharacterSheetJsonV1( val power: Int, val charisma: Int, // sub characteristics - val movement: Int, + val movement: Int?, val currentHp: Int, - val maxHp: Int, + val maxHp: Int?, val currentPP: Int, - val maxPP: Int, - val damageBonus: String, - val armor: Int, + val maxPP: Int?, + val damageBonus: String?, + val armor: Int?, // skills val skills: List, // occupations @@ -33,33 +33,19 @@ data class CharacterSheetJsonV1( ) : CharacterSheetJson { @Serializable data class Skill( + val id: String, val label: String, - val value: Int, + val base: String, + val bonus: Int, + val level: Int, + val occupation: Boolean, val used: Boolean, ) @Serializable data class Roll( + val id: String, val label: String, val roll: String, ) - - companion object { - const val COMBAT = "Bagarre" - const val DODGE = "Esquive" - const val GRAB = "Saisie" - const val THROW = "Lancer" - const val ATHLETICS = "Athlétisme" - const val ACROBATICS = "Acrobatie" - const val PERCEPTION = "Perception" - const val SEARCH = "Recherche" - const val EMPATHY = "Empathie" - const val PERSUASION = "Persuasion" - const val INTIMIDATION = "Intimidation" - const val SPIEL = "Baratin" - const val BARGAIN = "Marchandage" - const val DISCRETION = "Discrétion" - const val SLEIGHT_OF_HAND = "Escamotage" - const val AID = "Premiers soins" - } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/common/SkillFieldFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/common/SkillFieldFactory.kt new file mode 100644 index 0000000..eb1dbbb --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/common/SkillFieldFactory.kt @@ -0,0 +1,86 @@ +package com.pixelized.desktop.lwa.screen.characterSheet.common + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete +import androidx.compose.runtime.mutableStateOf +import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.SkillFieldUio +import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.textfield.TextFieldWrapperUio +import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.option.ActionOption +import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.option.CheckedOption +import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.option.OptionUio +import lwacharactersheet.composeapp.generated.resources.Res +import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__delete__label +import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__occupation__label +import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__base_label +import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__bonus_label +import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__level_label +import org.jetbrains.compose.resources.getString +import java.util.UUID + +class SkillFieldFactory { + + + suspend fun createSkill( + id: String = UUID.randomUUID().toString(), + label: String, + labelValue: String = "", + baseValue: String = "", + bonusValue: String = "", + levelValue: String = "", + options: List = emptyList(), + ): SkillFieldUio { + return SkillFieldUio( + id = id, + label = createWrapper( + label = label, + value = labelValue, + ), + base = createWrapper( + label = getString(Res.string.character_sheet_edit__skills__base_label), + value = baseValue, + ), + bonus = createWrapper( + label = getString(Res.string.character_sheet_edit__skills__bonus_label), + value = bonusValue, + ), + level = createWrapper( + label = getString(Res.string.character_sheet_edit__skills__level_label), + value = levelValue, + ), + options = options, + ) + } + + fun createWrapper( + enable: Boolean = true, + label: String = "", + value: String = "", + ): TextFieldWrapperUio { + val state = mutableStateOf(value) + return TextFieldWrapperUio( + enable = enable, + label = label, + value = state, + onValueChange = { state.value = it }, + ) + } + + suspend fun deleteOption(onDelete: () -> Unit) = ActionOption.DeleteOptionUio( + icon = Icons.Default.Delete, + label = getString(Res.string.character_sheet_edit__delete__label), + onOption = onDelete, + ) + + suspend fun occupationOption(checked: Boolean) = mutableStateOf(checked).let { state -> + CheckedOption.OccupationOption( + checked = state, + label = getString(Res.string.character_sheet_edit__occupation__label), + onOption = { state.value = state.value.not() }, + ) + } +} + +val List.occupation: Boolean + get() = this.firstNotNullOfOrNull { + if (it is CheckedOption.OccupationOption) it.checked.value else null + } ?: false \ No newline at end of file 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 1502be5..31e7a24 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 @@ -1,7 +1,9 @@ package com.pixelized.desktop.lwa.screen.characterSheet.detail +import com.pixelized.desktop.lwa.business.SkillValueComputationUseCase import com.pixelized.desktop.lwa.composable.tooltip.TooltipUio import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheet +import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheet.CommonSkillId 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 @@ -46,9 +48,10 @@ import lwacharactersheet.composeapp.generated.resources.tooltip__sub_characteris import lwacharactersheet.composeapp.generated.resources.tooltip__sub_characteristics__movement import lwacharactersheet.composeapp.generated.resources.tooltip__sub_characteristics__power_point import org.jetbrains.compose.resources.getString -import kotlin.math.max -class CharacterSheetFactory { +class CharacterSheetFactory( + private val skillUseCase: SkillValueComputationUseCase, +) { companion object { const val HP = "HP" @@ -56,17 +59,17 @@ class CharacterSheetFactory { } suspend fun convertToUio( - model: CharacterSheet, + sheet: CharacterSheet, diminished: Int, ): CharacterSheetPageUio { return CharacterSheetPageUio( - id = model.id, - name = model.name, + id = sheet.id, + name = sheet.name, characteristics = listOf( Characteristic( id = "STR", label = getString(Res.string.character_sheet__characteristics__str), - value = "${model.strength}", + value = "${sheet.strength}", tooltips = TooltipUio( title = getString(Res.string.character_sheet__characteristics__str), description = getString(Res.string.tooltip__characteristics__strength), @@ -76,7 +79,7 @@ class CharacterSheetFactory { Characteristic( id = "DEX", label = getString(Res.string.character_sheet__characteristics__dex), - value = "${model.dexterity}", + value = "${sheet.dexterity}", tooltips = TooltipUio( title = getString(Res.string.character_sheet__characteristics__dex), description = getString(Res.string.tooltip__characteristics__dexterity), @@ -86,7 +89,7 @@ class CharacterSheetFactory { Characteristic( id = "CON", label = getString(Res.string.character_sheet__characteristics__con), - value = "${model.constitution}", + value = "${sheet.constitution}", tooltips = TooltipUio( title = getString(Res.string.character_sheet__characteristics__con), description = getString(Res.string.tooltip__characteristics__constitution), @@ -96,7 +99,7 @@ class CharacterSheetFactory { Characteristic( id = "HEI", label = getString(Res.string.character_sheet__characteristics__hei), - value = "${model.height}", + value = "${sheet.height}", tooltips = TooltipUio( title = getString(Res.string.character_sheet__characteristics__hei), description = getString(Res.string.tooltip__characteristics__height), @@ -106,7 +109,7 @@ class CharacterSheetFactory { Characteristic( id = "INT", label = getString(Res.string.character_sheet__characteristics__int), - value = "${model.intelligence}", + value = "${sheet.intelligence}", tooltips = TooltipUio( title = getString(Res.string.character_sheet__characteristics__int), description = getString(Res.string.tooltip__characteristics__intelligence), @@ -116,7 +119,7 @@ class CharacterSheetFactory { Characteristic( id = "POW", label = getString(Res.string.character_sheet__characteristics__pow), - value = "${model.power}", + value = "${sheet.power}", tooltips = TooltipUio( title = getString(Res.string.character_sheet__characteristics__pow), description = getString(Res.string.tooltip__characteristics__power), @@ -126,7 +129,7 @@ class CharacterSheetFactory { Characteristic( id = "CHA", label = getString(Res.string.character_sheet__characteristics__cha), - value = "${model.charisma}", + value = "${sheet.charisma}", tooltips = TooltipUio( title = getString(Res.string.character_sheet__characteristics__cha), description = getString(Res.string.tooltip__characteristics__charisma), @@ -138,7 +141,7 @@ class CharacterSheetFactory { Characteristic( id = "MOV", label = getString(Res.string.character_sheet__sub_characteristics__movement), - value = "${model.movement}", + value = "${sheet.movement}", tooltips = TooltipUio( title = getString(Res.string.character_sheet__sub_characteristics__movement), description = getString(Res.string.tooltip__sub_characteristics__movement), @@ -148,7 +151,7 @@ class CharacterSheetFactory { Characteristic( id = HP, label = getString(Res.string.character_sheet__sub_characteristics__hit_point), - value = "${model.currentHp}/${model.maxHp}", + value = "${sheet.currentHp}/${sheet.maxHp}", tooltips = TooltipUio( title = getString(Res.string.character_sheet__sub_characteristics__hit_point), description = getString(Res.string.tooltip__sub_characteristics__hit_point), @@ -158,7 +161,7 @@ class CharacterSheetFactory { Characteristic( id = PP, label = getString(Res.string.character_sheet__sub_characteristics__power_point), - value = "${model.currentPP}/${model.maxPP}", + value = "${sheet.currentPp}/${sheet.maxPP}", tooltips = TooltipUio( title = getString(Res.string.character_sheet__sub_characteristics__power_point), description = getString(Res.string.tooltip__sub_characteristics__power_point), @@ -168,7 +171,7 @@ class CharacterSheetFactory { Characteristic( id = "DMG", label = getString(Res.string.character_sheet__sub_characteristics__damage_bonus), - value = model.damageBonus, + value = sheet.damageBonus, tooltips = TooltipUio( title = getString(Res.string.character_sheet__sub_characteristics__damage_bonus), description = getString(Res.string.tooltip__sub_characteristics__bonus_damage), @@ -178,7 +181,7 @@ class CharacterSheetFactory { Characteristic( id = "ARMOR", label = getString(Res.string.character_sheet__sub_characteristics__armor), - value = "${model.armor}", + value = "${sheet.armor}", tooltips = TooltipUio( title = getString(Res.string.character_sheet__sub_characteristics__armor), description = getString(Res.string.tooltip__sub_characteristics__armor), @@ -186,69 +189,69 @@ class CharacterSheetFactory { editable = false, ), ), - skills = model.skills.mapNotNull { skill -> - if (skill.value > 0) { - val description = when (skill.label) { - CharacterSheet.COMBAT -> getString(Res.string.tooltip__skills__combat) - CharacterSheet.DODGE -> getString(Res.string.tooltip__skills__dodge) - CharacterSheet.GRAB -> getString(Res.string.tooltip__skills__grab) - CharacterSheet.THROW -> getString(Res.string.tooltip__skills__throw) - CharacterSheet.ATHLETICS -> getString(Res.string.tooltip__skills__athletics) - CharacterSheet.ACROBATICS -> getString(Res.string.tooltip__skills__acrobatics) - CharacterSheet.PERCEPTION -> getString(Res.string.tooltip__skills__perception) - CharacterSheet.SEARCH -> getString(Res.string.tooltip__skills__search) - CharacterSheet.EMPATHY -> getString(Res.string.tooltip__skills__empathy) - CharacterSheet.PERSUASION -> getString(Res.string.tooltip__skills__persuasion) - CharacterSheet.INTIMIDATION -> getString(Res.string.tooltip__skills__intimidation) - CharacterSheet.SPIEL -> getString(Res.string.tooltip__skills__spiel) - CharacterSheet.BARGAIN -> getString(Res.string.tooltip__skills__bargain) - CharacterSheet.DISCRETION -> getString(Res.string.tooltip__skills__discretion) - CharacterSheet.SLEIGHT_OF_HAND -> getString(Res.string.tooltip__skills__sleight_of_hand) - CharacterSheet.AID -> getString(Res.string.tooltip__skills__aid) - else -> null - } - Node( - label = skill.label, - value = skill.value.diminished(diminished), - tooltips = description?.let { - TooltipUio( - title = skill.label, - description = it, - ) - }, - used = skill.used, - ) - } else { - null + commonSkills = sheet.commonSkills.map { skill -> + val description = when (skill.id) { + CommonSkillId.COMBAT_ID -> getString(Res.string.tooltip__skills__combat) + CommonSkillId.DODGE_ID -> getString(Res.string.tooltip__skills__dodge) + CommonSkillId.GRAB_ID -> getString(Res.string.tooltip__skills__grab) + CommonSkillId.THROW_ID -> getString(Res.string.tooltip__skills__throw) + CommonSkillId.ATHLETICS_ID -> getString(Res.string.tooltip__skills__athletics) + CommonSkillId.ACROBATICS_ID -> getString(Res.string.tooltip__skills__acrobatics) + CommonSkillId.PERCEPTION_ID -> getString(Res.string.tooltip__skills__perception) + CommonSkillId.SEARCH_ID -> getString(Res.string.tooltip__skills__search) + CommonSkillId.EMPATHY_ID -> getString(Res.string.tooltip__skills__empathy) + CommonSkillId.PERSUASION_ID -> getString(Res.string.tooltip__skills__persuasion) + CommonSkillId.INTIMIDATION_ID -> getString(Res.string.tooltip__skills__intimidation) + CommonSkillId.SPIEL_ID -> getString(Res.string.tooltip__skills__spiel) + CommonSkillId.BARGAIN_ID -> getString(Res.string.tooltip__skills__bargain) + CommonSkillId.DISCRETION_ID -> getString(Res.string.tooltip__skills__discretion) + CommonSkillId.SLEIGHT_OF_HAND_ID -> getString(Res.string.tooltip__skills__sleight_of_hand) + CommonSkillId.AID_ID -> getString(Res.string.tooltip__skills__aid) + else -> null } + Node( + label = skill.label, + value = skillUseCase.computeSkillValue( + sheet = sheet, + skill = skill, + diminished = diminished, + ), + tooltips = description?.let { + TooltipUio( + title = skill.label, + description = it, + ) + }, + used = skill.used, + ) }, - occupations = model.occupations.mapNotNull { - if (it.value > 0) { - Node( - label = it.label, - value = it.value.diminished(diminished), - used = it.used, - ) - } else { - null - } + specialSKills = sheet.specialSkills.map { + Node( + label = it.label, + value = skillUseCase.computeSkillValue( + sheet = sheet, + skill = it, + diminished = diminished, + ), + used = it.used, + ) }, - magics = model.magics.mapNotNull { - if (it.value > 0) { - Node( - label = it.label, - value = it.value.diminished(diminished), - used = it.used, - ) - } else { - null - } + magicsSkills = sheet.magicSkills.map { + Node( + label = it.label, + value = skillUseCase.computeSkillValue( + sheet = sheet, + skill = it, + diminished = diminished, + ), + used = it.used, + ) }, - rolls = model.rolls.mapNotNull { + actions = sheet.actions.mapNotNull { if (it.roll.isNotEmpty()) { CharacterSheetPageUio.Roll( label = it.label, - value = it.roll, + value = "TODO",//it.roll, ) } else { null @@ -256,8 +259,4 @@ class CharacterSheetFactory { } ) } -} - -fun Int.diminished(value: Int): Int { - return max(0, this - value) } \ No newline at end of file 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 b160a4a..64c5b52 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 @@ -41,6 +41,7 @@ import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.MoreVert import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf @@ -61,6 +62,7 @@ import com.pixelized.desktop.lwa.composable.tooltip.TooltipUio import com.pixelized.desktop.lwa.navigation.screen.LocalScreenController import com.pixelized.desktop.lwa.navigation.screen.destination.navigateToCharacterSheetEdit import com.pixelized.desktop.lwa.navigation.window.LocalWindow +import com.pixelized.desktop.lwa.navigation.window.destination.CharacterSheetWindow 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.characterSheet.detail.dialog.DiminishedStatDialog @@ -70,9 +72,9 @@ import kotlinx.coroutines.launch import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.character_sheet__delete__label import lwacharactersheet.composeapp.generated.resources.character_sheet__edit__label -import lwacharactersheet.composeapp.generated.resources.character_sheet__magics__title -import lwacharactersheet.composeapp.generated.resources.character_sheet__occupations_title -import lwacharactersheet.composeapp.generated.resources.character_sheet__skills__title +import lwacharactersheet.composeapp.generated.resources.character_sheet__skills__common_title +import lwacharactersheet.composeapp.generated.resources.character_sheet__skills__magic_title +import lwacharactersheet.composeapp.generated.resources.character_sheet__skills__special_title import lwacharactersheet.composeapp.generated.resources.character_sheet__sub_characteristics__title import lwacharactersheet.composeapp.generated.resources.ic_d20_32dp import lwacharactersheet.composeapp.generated.resources.ic_skull_32dp @@ -86,10 +88,10 @@ data class CharacterSheetPageUio( val name: String, val characteristics: List, val subCharacteristics: List, - val skills: List, - val occupations: List, - val magics: List, - val rolls: List, + val commonSkills: List, + val specialSKills: List, + val magicsSkills: List, + val actions: List, ) { @Stable data class Characteristic( @@ -126,6 +128,10 @@ fun CharacterSheetPage( val scope = rememberCoroutineScope() val blurController = remember { BlurContentController() } + LaunchedEffect(Unit) { + window.resize(size = CharacterSheetWindow.size) + } + Surface( modifier = Modifier.fillMaxSize(), ) { @@ -365,7 +371,7 @@ fun CharacterSheetPageContent( modifier = Modifier .verticalScroll(state = scrollState) .padding(paddingValues) - .padding(horizontal = 24.dp, vertical = 16.dp), + .padding(all = 16.dp), horizontalArrangement = Arrangement.spacedBy(space = 16.dp), ) { Column( @@ -427,9 +433,9 @@ fun CharacterSheetPageContent( .padding(bottom = 8.dp), style = MaterialTheme.typography.caption, textAlign = TextAlign.Center, - text = stringResource(Res.string.character_sheet__skills__title), + text = stringResource(Res.string.character_sheet__skills__common_title), ) - characterSheet.skills.forEach { skill -> + characterSheet.commonSkills.forEach { skill -> Skill( modifier = Modifier.cell(), node = skill, @@ -449,9 +455,9 @@ fun CharacterSheetPageContent( modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp), style = MaterialTheme.typography.caption, textAlign = TextAlign.Center, - text = stringResource(Res.string.character_sheet__occupations_title), + text = stringResource(Res.string.character_sheet__skills__special_title), ) - characterSheet.occupations.forEach { occupation -> + characterSheet.specialSKills.forEach { occupation -> Skill( modifier = Modifier.cell(), node = occupation, @@ -471,9 +477,9 @@ fun CharacterSheetPageContent( modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp), style = MaterialTheme.typography.caption, textAlign = TextAlign.Center, - text = stringResource(Res.string.character_sheet__magics__title), + text = stringResource(Res.string.character_sheet__skills__magic_title), ) - characterSheet.magics.forEach { magic -> + characterSheet.magicsSkills.forEach { magic -> Skill( modifier = Modifier.cell(), node = magic, @@ -483,7 +489,7 @@ fun CharacterSheetPageContent( } } } - characterSheet.rolls.forEach { roll -> + characterSheet.actions.forEach { roll -> Roll( modifier = Modifier.cell(), label = roll.label, 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 3cc9e89..6bc3d7e 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 @@ -73,7 +73,7 @@ class CharacterSheetViewModel( } ).collectAsState { (sheet, diminished) -> sheet?.let { model -> - runBlocking { factory.convertToUio(model = model, diminished = diminished) } + runBlocking { factory.convertToUio(sheet = model, diminished = diminished) } } } @@ -84,21 +84,21 @@ class CharacterSheetViewModel( fun onUseSkill(skill: CharacterSheetPageUio.Node) { repository.characterSheetFlow(id = argument.id).value?.let { sheet -> - val skills = sheet.skills.map { + val skills = sheet.commonSkills.map { if (it.label == skill.label) it.copy(used = it.used.not()) else it } - val occupations = sheet.occupations.map { + val occupations = sheet.specialSkills.map { if (it.label == skill.label) it.copy(used = it.used.not()) else it } - val magics = sheet.magics.map { + val magics = sheet.magicSkills.map { if (it.label == skill.label) it.copy(used = it.used.not()) else it } repository.save( characterSheet = sheet.copy( - skills = skills, - occupations = occupations, - magics = magics, + commonSkills = skills, + specialSkills = occupations, + magicSkills = magics, ) ) } @@ -137,7 +137,7 @@ class CharacterSheetViewModel( CharacterSheetFactory.PP -> { val value = mutableStateOf( - "${sheet.currentPP}".let { + "${sheet.currentPp}".let { TextFieldValue(text = it, selection = TextRange(it.length)) } ) @@ -166,7 +166,7 @@ class CharacterSheetViewModel( 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))) + CharacterSheetFactory.PP -> sheet?.copy(currentPp = max(0, min(sheet.maxPP, value))) else -> null } updated?.let { diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/edit/CharacterSheetEditFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/edit/CharacterSheetEditFactory.kt index 3040477..e79673a 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/edit/CharacterSheetEditFactory.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/edit/CharacterSheetEditFactory.kt @@ -1,9 +1,16 @@ package com.pixelized.desktop.lwa.screen.characterSheet.edit -import com.pixelized.desktop.lwa.business.DamageBonusUseCase.bonusDamage +import androidx.compose.runtime.State +import androidx.compose.runtime.derivedStateOf +import com.pixelized.desktop.lwa.business.DamageBonusUseCase import com.pixelized.desktop.lwa.business.SkillNormalizerUseCase.normalize +import com.pixelized.desktop.lwa.parser.arithmetic.Arithmetic +import com.pixelized.desktop.lwa.parser.arithmetic.ArithmeticParser +import com.pixelized.desktop.lwa.parser.arithmetic.Instruction import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheet -import com.pixelized.desktop.lwa.screen.characterSheet.edit.CharacterSheetEditPageUio.SkillGroup +import com.pixelized.desktop.lwa.screen.characterSheet.common.SkillFieldFactory +import com.pixelized.desktop.lwa.screen.characterSheet.common.occupation +import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.BaseSkillFieldUio import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.FieldUio import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__characteristics__cha @@ -13,86 +20,125 @@ import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__ch import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__characteristics__int import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__characteristics__pow import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__characteristics__str -import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__characteristics__title -import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__magic__add_action -import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__magic__title import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__name_placeholder -import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__occupation__add_action -import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__occupation__title import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__acrobatics -import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__add_action import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__aid import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__athletics import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__bargain +import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__bonus_label import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__combat import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__discretion import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__dodge import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__empathy import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__grab import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__intimidation +import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__level_label +import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__magic_title import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__perception import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__persuasion import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__search import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__sleight_of_hand +import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__special_title import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__spiel import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__throw -import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__title import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__sub_characteristics__armor import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__sub_characteristics__damage_bonus import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__sub_characteristics__hit_point import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__sub_characteristics__movement import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__sub_characteristics__power_point -import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__sub_characteristics__title import org.jetbrains.compose.resources.getString import java.util.UUID import kotlin.math.ceil import kotlin.math.max +import kotlin.math.min -class CharacterSheetEditFactory { - - fun convertToModel(sheet: CharacterSheetEditPageUio): CharacterSheet { +class CharacterSheetEditFactory( + private val bonusDamageUseCase: DamageBonusUseCase, + private val skillFactory: SkillFieldFactory, + private val parser: ArithmeticParser, +) { + fun updateCharacterSheet( + currentSheet: CharacterSheet?, + editedSheet: 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 { + id = editedSheet.id, + name = editedSheet.name.value.value, + strength = editedSheet.strength.unpack(), + dexterity = editedSheet.dexterity.unpack(), + constitution = editedSheet.constitution.unpack(), + height = editedSheet.height.unpack(), + intelligence = editedSheet.intelligence.unpack(), + power = editedSheet.power.unpack(), + charisma = editedSheet.charisma.unpack(), + overrideMovement = editedSheet.movement.value.value.isNotBlank(), + movement = editedSheet.movement.unpack(), + overrideMaxHp = editedSheet.maxHp.value.value.isNotBlank(), + maxHp = editedSheet.maxHp.unpack(), + currentHp = editedSheet.maxHp.unpack().let { + max(0, min(it, currentSheet?.currentHp ?: it)) + }, + overrideMaxPP = editedSheet.maxPP.value.value.isNotBlank(), + maxPP = editedSheet.maxPP.unpack(), + currentPp = editedSheet.maxPP.unpack().let { + max(0, min(it, currentSheet?.currentPp ?: it)) + }, + overrideDamageBonus = editedSheet.damageBonus.value.value.isNotBlank(), + damageBonus = editedSheet.damageBonus.unpack(), + overrideArmor = editedSheet.armor.value.value.isNotBlank(), + armor = editedSheet.armor.unpack(), + commonSkills = editedSheet.commonSkills.map { editedSkill -> + val currentSkill = currentSheet?.commonSkills?.firstOrNull { + it.id == editedSkill.id + } CharacterSheet.Skill( - label = it.label.value, - value = it.unpack(), - used = false, + id = editedSkill.id, + label = editedSkill.label, + base = listOf( + Arithmetic( + sign = 1, + instruction = Instruction.Flat(editedSkill.base.value) + ) + ), + bonus = editedSkill.bonus.value.value.toIntOrNull() ?: 0, + level = editedSkill.level.value.value.toIntOrNull() ?: 0, + occupation = editedSkill.option.checked.value, + used = currentSkill?.used ?: false, ) }, - occupations = sheet.skills[3].fields.map { + specialSkills = editedSheet.specialSkills.map { editedSkill -> + val currentSkill = currentSheet?.specialSkills?.firstOrNull { + it.id == editedSkill.id + } CharacterSheet.Skill( - label = it.label.value, - value = it.unpack(), - used = false, + id = editedSkill.id, + label = editedSkill.label.value.value, + base = parser.parse(editedSkill.base.value.value), + bonus = editedSkill.bonus.value.value.toIntOrNull() ?: 0, + level = editedSkill.level.value.value.toIntOrNull() ?: 0, + occupation = editedSkill.options.occupation, + used = currentSkill?.used ?: false, ) }, - magics = sheet.skills[4].fields.map { + magicSkills = editedSheet.magicSkills.map { editedSkill -> + val currentSkill = currentSheet?.magicSkills?.firstOrNull { + it.id == editedSkill.id + } CharacterSheet.Skill( - label = it.label.value, - value = it.unpack(), - used = false, + id = editedSkill.id, + label = editedSkill.label.value.value, + base = parser.parse(editedSkill.base.value.value), + bonus = editedSkill.bonus.value.value.toIntOrNull() ?: 0, + level = editedSkill.level.value.value.toIntOrNull() ?: 0, + occupation = editedSkill.options.occupation, + used = currentSkill?.used ?: false, ) }, - rolls = sheet.rolls.map { + actions = editedSheet.actions.map { CharacterSheet.Roll( + id = "", // TODO label = it.label.value, - roll = it.unpack(), + roll = parser.parse(value = it.unpack()), ) }, ) @@ -100,41 +146,42 @@ class CharacterSheetEditFactory { suspend fun convertToUio( sheet: CharacterSheet?, + onDeleteSkill: (skillId: String) -> Unit, ): CharacterSheetEditPageUio { val str = FieldUio.create( initialLabel = getString(Res.string.character_sheet_edit__characteristics__str), initialValue = sheet?.strength?.toString() ?: "", - valuePlaceHolder = { "0" }, + valuePlaceHolder = { "10" }, ) val dex = FieldUio.create( initialLabel = getString(Res.string.character_sheet_edit__characteristics__dex), initialValue = sheet?.dexterity?.toString() ?: "", - valuePlaceHolder = { "0" } + valuePlaceHolder = { "11" } ) val con = FieldUio.create( initialLabel = getString(Res.string.character_sheet_edit__characteristics__con), initialValue = sheet?.constitution?.toString() ?: "", - valuePlaceHolder = { "0" } + valuePlaceHolder = { "15" } ) val hei = FieldUio.create( initialLabel = getString(Res.string.character_sheet_edit__characteristics__hei), initialValue = sheet?.height?.toString() ?: "", - valuePlaceHolder = { "0" } + valuePlaceHolder = { "13" } ) val int = FieldUio.create( initialLabel = getString(Res.string.character_sheet_edit__characteristics__int), initialValue = sheet?.intelligence?.toString() ?: "", - valuePlaceHolder = { "0" } + valuePlaceHolder = { "9" } ) val pow = FieldUio.create( initialLabel = getString(Res.string.character_sheet_edit__characteristics__pow), initialValue = sheet?.power?.toString() ?: "", - valuePlaceHolder = { "0" } + valuePlaceHolder = { "15" } ) val cha = FieldUio.create( initialLabel = getString(Res.string.character_sheet_edit__characteristics__cha), initialValue = sheet?.charisma?.toString() ?: "", - valuePlaceHolder = { "0" } + valuePlaceHolder = { "7" } ) fun str(): Int = str.unpack() ?: 0 @@ -145,6 +192,9 @@ class CharacterSheetEditFactory { fun pow(): Int = pow.unpack() ?: 0 fun cha(): Int = cha.unpack() ?: 0 + val specialSkillsLabel = getString(Res.string.character_sheet_edit__skills__special_title) + val magicSkillsLabel = getString(Res.string.character_sheet_edit__skills__magic_title) + return CharacterSheetEditPageUio( id = sheet?.id ?: UUID.randomUUID().toString(), name = FieldUio.create( @@ -152,153 +202,207 @@ class CharacterSheetEditFactory { initialLabel = getString(Res.string.character_sheet_edit__name_placeholder), initialValue = sheet?.name ?: "" ), - skills = listOf( - SkillGroup( - type = SkillGroup.Type.CHARACTERISTICS, - title = getString(Res.string.character_sheet_edit__characteristics__title), - fields = listOf(str, dex, con, hei, int, pow, cha), + strength = str, + dexterity = dex, + constitution = con, + height = hei, + intelligence = int, + power = pow, + charisma = cha, + movement = FieldUio.create( + initialLabel = getString(Res.string.character_sheet_edit__sub_characteristics__movement), + initialValue = (if (sheet?.overrideMovement == true) "${sheet.movement}" else null) + ?: "", + valuePlaceHolder = { "10" } + ), + maxHp = FieldUio.create( + initialLabel = getString(Res.string.character_sheet_edit__sub_characteristics__hit_point), + initialValue = (if (sheet?.overrideMaxHp == true) "${sheet.maxHp}" else null) ?: "", + valuePlaceHolder = { "${ceil((con() + hei()) / 2f).toInt()}" } + ), + maxPP = FieldUio.create( + initialLabel = getString(Res.string.character_sheet_edit__sub_characteristics__power_point), + initialValue = (if (sheet?.overrideMaxPP == true) "${sheet.maxPP}" else null) ?: "", + valuePlaceHolder = { "${pow()}" } + ), + damageBonus = FieldUio.create( + initialLabel = getString(Res.string.character_sheet_edit__sub_characteristics__damage_bonus), + initialValue = (if (sheet?.overrideDamageBonus == true) sheet.damageBonus else null) + ?: "", + valuePlaceHolder = { + bonusDamageUseCase.bonusDamage( + strength = str(), + height = hei() + ) + } + ), + armor = FieldUio.create( + initialLabel = getString(Res.string.character_sheet_edit__sub_characteristics__armor), + initialValue = (if (sheet?.overrideArmor == true) sheet.armor.toString() else null) + ?: "", + valuePlaceHolder = { "0" } + ), + commonSkills = listOf( + createBaseSkill( + sheet = sheet, + id = CharacterSheet.CommonSkillId.COMBAT_ID, + label = getString(Res.string.character_sheet_edit__skills__combat), + base = derivedStateOf { normalize(dex() * 2) }, ), - SkillGroup( - type = SkillGroup.Type.SUB_CHARACTERISTICS, - title = getString(Res.string.character_sheet_edit__sub_characteristics__title), - fields = listOf( - FieldUio.create( - initialLabel = getString(Res.string.character_sheet_edit__sub_characteristics__movement), - initialValue = sheet?.movement?.toString() ?: "", - valuePlaceHolder = { "10" } - ), - FieldUio.create( - initialLabel = getString(Res.string.character_sheet_edit__sub_characteristics__hit_point), - initialValue = sheet?.maxHp?.toString() ?: "", - valuePlaceHolder = { "${ceil((con() + hei()) / 2f).toInt()}" } - ), - FieldUio.create( - initialLabel = getString(Res.string.character_sheet_edit__sub_characteristics__power_point), - initialValue = sheet?.maxPP?.toString() ?: "", - valuePlaceHolder = { "${pow()}" } - ), - FieldUio.create( - initialLabel = getString(Res.string.character_sheet_edit__sub_characteristics__damage_bonus), - initialValue = sheet?.damageBonus ?: "", - valuePlaceHolder = { bonusDamage(strength = str(), height = hei()) } - ), - FieldUio.create( - initialLabel = getString(Res.string.character_sheet_edit__sub_characteristics__armor), - initialValue = sheet?.armor?.toString() ?: "", - valuePlaceHolder = { "0" } - ), - ), + createBaseSkill( + sheet = sheet, + id = CharacterSheet.CommonSkillId.DODGE_ID, + label = getString(Res.string.character_sheet_edit__skills__dodge), + base = derivedStateOf { normalize(dex() * 2) }, ), - SkillGroup( - type = SkillGroup.Type.SKILLS, - title = getString(Res.string.character_sheet_edit__skills__title), - action = getString(Res.string.character_sheet_edit__skills__add_action), - fields = sheet?.skills?.map { - FieldUio.create( - initialLabel = it.label, - initialValue = it.value.toString(), - ) - } ?: listOf( - FieldUio.create( - initialLabel = getString(Res.string.character_sheet_edit__skills__combat), - valuePlaceHolder = { "${normalize(dex() * 2)}" }, - ), - FieldUio.create( - initialLabel = getString(Res.string.character_sheet_edit__skills__dodge), - valuePlaceHolder = { "${normalize(dex() * 2)}" }, - ), - FieldUio.create( - initialLabel = getString(Res.string.character_sheet_edit__skills__grab), - valuePlaceHolder = { "${normalize(str() + hei())}" }, - ), - FieldUio.create( - initialLabel = getString(Res.string.character_sheet_edit__skills__throw), - valuePlaceHolder = { "${normalize(str() + dex())}" }, - ), - FieldUio.create( - initialLabel = getString(Res.string.character_sheet_edit__skills__athletics), - valuePlaceHolder = { "${normalize(str() + con() * 2)}" }, - ), - FieldUio.create( - initialLabel = getString(Res.string.character_sheet_edit__skills__acrobatics), - valuePlaceHolder = { "${normalize(dex() + con() * 2)}" }, - ), - FieldUio.create( - initialLabel = getString(Res.string.character_sheet_edit__skills__perception), - valuePlaceHolder = { "${normalize(10 + int() * 2)}" }, - ), - FieldUio.create( - initialLabel = getString(Res.string.character_sheet_edit__skills__search), - valuePlaceHolder = { "${normalize(10 + int() * 2)}" }, - ), - FieldUio.create( - initialLabel = getString(Res.string.character_sheet_edit__skills__empathy), - valuePlaceHolder = { "${normalize(cha() + int())}" }, - ), - FieldUio.create( - initialLabel = getString(Res.string.character_sheet_edit__skills__persuasion), - valuePlaceHolder = { "${normalize(cha() * 3)}" }, - ), - FieldUio.create( - initialLabel = getString(Res.string.character_sheet_edit__skills__intimidation), - valuePlaceHolder = { "${normalize(cha() + max(pow(), hei()) * 2)}" }, - ), - FieldUio.create( - initialLabel = getString(Res.string.character_sheet_edit__skills__spiel), - valuePlaceHolder = { "${normalize(cha() * 2 + int())}" }, - ), - FieldUio.create( - initialLabel = getString(Res.string.character_sheet_edit__skills__bargain), - valuePlaceHolder = { "${normalize(cha() * 2)}" }, - ), - FieldUio.create( - initialLabel = getString(Res.string.character_sheet_edit__skills__discretion), - valuePlaceHolder = { "${normalize(cha() + dex() * 2 - hei())}" }, - ), - FieldUio.create( - initialLabel = getString(Res.string.character_sheet_edit__skills__sleight_of_hand), - valuePlaceHolder = { "${normalize(dex() * 2)}" }, - ), - FieldUio.create( - initialLabel = getString(Res.string.character_sheet_edit__skills__aid), - valuePlaceHolder = { "${normalize(int() + dex())}" }, - ), - ), + createBaseSkill( + sheet = sheet, + id = CharacterSheet.CommonSkillId.GRAB_ID, + label = getString(Res.string.character_sheet_edit__skills__grab), + base = derivedStateOf { normalize(str() + hei()) }, ), - SkillGroup( - type = SkillGroup.Type.OCCUPATIONS, - title = getString(Res.string.character_sheet_edit__occupation__title), - action = getString(Res.string.character_sheet_edit__occupation__add_action), - fields = sheet?.occupations?.map { - FieldUio.create( - initialLabel = it.label, - initialValue = it.value.toString(), - valuePlaceHolder = { "40" } - ) - } ?: emptyList(), + createBaseSkill( + sheet = sheet, + id = CharacterSheet.CommonSkillId.THROW_ID, + label = getString(Res.string.character_sheet_edit__skills__throw), + base = derivedStateOf { normalize(str() + dex()) }, ), - SkillGroup( - type = SkillGroup.Type.MAGICS, - title = getString(Res.string.character_sheet_edit__magic__title), - action = getString(Res.string.character_sheet_edit__magic__add_action), - fields = sheet?.magics?.map { - FieldUio.create( - initialLabel = it.label, - initialValue = it.value.toString() - ) - } ?: emptyList(), + createBaseSkill( + sheet = sheet, + id = CharacterSheet.CommonSkillId.ATHLETICS_ID, + label = getString(Res.string.character_sheet_edit__skills__athletics), + base = derivedStateOf { normalize(str() + con() * 2) }, + ), + createBaseSkill( + sheet = sheet, + id = CharacterSheet.CommonSkillId.ACROBATICS_ID, + label = getString(Res.string.character_sheet_edit__skills__acrobatics), + base = derivedStateOf { normalize(dex() + con() * 2) }, + ), + createBaseSkill( + sheet = sheet, + id = CharacterSheet.CommonSkillId.PERCEPTION_ID, + label = getString(Res.string.character_sheet_edit__skills__perception), + base = derivedStateOf { normalize(10 + int() * 2) }, + ), + createBaseSkill( + sheet = sheet, + id = CharacterSheet.CommonSkillId.SEARCH_ID, + label = getString(Res.string.character_sheet_edit__skills__search), + base = derivedStateOf { normalize(10 + int() * 2) }, + ), + createBaseSkill( + sheet = sheet, + id = CharacterSheet.CommonSkillId.EMPATHY_ID, + label = getString(Res.string.character_sheet_edit__skills__empathy), + base = derivedStateOf { normalize(cha() + int()) }, + ), + createBaseSkill( + sheet = sheet, + id = CharacterSheet.CommonSkillId.PERSUASION_ID, + label = getString(Res.string.character_sheet_edit__skills__persuasion), + base = derivedStateOf { normalize(cha() * 3) }, + ), + createBaseSkill( + sheet = sheet, + id = CharacterSheet.CommonSkillId.INTIMIDATION_ID, + label = getString(Res.string.character_sheet_edit__skills__intimidation), + base = derivedStateOf { normalize(cha() + max(pow(), hei()) * 2) }, + ), + createBaseSkill( + sheet = sheet, + id = CharacterSheet.CommonSkillId.SPIEL_ID, + label = getString(Res.string.character_sheet_edit__skills__spiel), + base = derivedStateOf { normalize(cha() * 2 + int()) }, + ), + createBaseSkill( + sheet = sheet, + id = CharacterSheet.CommonSkillId.BARGAIN_ID, + label = getString(Res.string.character_sheet_edit__skills__bargain), + base = derivedStateOf { normalize(cha() * 2) }, + ), + createBaseSkill( + sheet = sheet, + id = CharacterSheet.CommonSkillId.DISCRETION_ID, + label = getString(Res.string.character_sheet_edit__skills__discretion), + base = derivedStateOf { normalize(cha() + dex() * 2 - hei()) }, + ), + createBaseSkill( + sheet = sheet, + id = CharacterSheet.CommonSkillId.SLEIGHT_OF_HAND_ID, + label = getString(Res.string.character_sheet_edit__skills__sleight_of_hand), + base = derivedStateOf { normalize(dex() * 2) }, + ), + createBaseSkill( + sheet = sheet, + id = CharacterSheet.CommonSkillId.AID_ID, + label = getString(Res.string.character_sheet_edit__skills__aid), + base = derivedStateOf { normalize(int() + dex()) }, ), ), - rolls = sheet?.rolls?.map { - FieldUio.create( - isLabelEditable = true, - initialLabel = it.label, - initialValue = it.roll, + specialSkills = sheet?.specialSkills?.map { skill -> + skillFactory.createSkill( + id = skill.id, + label = specialSkillsLabel, + labelValue = skill.label, + baseValue = parser.convertInstructionToString(instructions = skill.base), + bonusValue = skill.bonus.takeIf { it > 0 }?.toString() ?: "", + levelValue = skill.level.takeIf { it > 0 }?.toString() ?: "", + options = run { + val current = sheet.specialSkills.firstOrNull { it.id == skill.id } + listOf( + skillFactory.occupationOption(checked = current?.occupation ?: false), + skillFactory.deleteOption { onDeleteSkill(skill.id) }, + ) + }, ) - } ?: emptyList() + } ?: emptyList(), + magicSkills = sheet?.magicSkills?.map { skill -> + skillFactory.createSkill( + id = skill.id, + label = magicSkillsLabel, + labelValue = skill.label, + baseValue = parser.convertInstructionToString(instructions = skill.base), + bonusValue = skill.bonus.takeIf { it > 0 }?.toString() ?: "", + levelValue = skill.level.takeIf { it > 0 }?.toString() ?: "", + options = run { + val current = sheet.magicSkills.firstOrNull { it.id == skill.id } + listOf( + skillFactory.occupationOption(checked = current?.occupation ?: false), + skillFactory.deleteOption { onDeleteSkill(skill.id) }, + ) + }, + ) + } ?: emptyList(), + actions = emptyList(), ) } + private suspend fun createBaseSkill( + sheet: CharacterSheet?, + id: String, + label: String, + base: State, + ): BaseSkillFieldUio { + val skill = sheet?.commonSkills?.firstOrNull { it.id == id } + + return BaseSkillFieldUio( + id = id, + label = label, + base = base, + bonus = skillFactory.createWrapper( + label = getString(Res.string.character_sheet_edit__skills__bonus_label), + value = skill?.bonus?.takeIf { it > 0 }?.toString() ?: "", + ), + level = skillFactory.createWrapper( + label = getString(Res.string.character_sheet_edit__skills__level_label), + value = skill?.level?.takeIf { it > 0 }?.toString() ?: "", + ), + option = skillFactory.occupationOption(skill?.occupation ?: false), + ) + } + + private inline fun FieldUio.unpack(): T { val tmp = value.value.ifBlank { valuePlaceHolder.value } return when (T::class) { diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/edit/CharacterSheetEditPage.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/edit/CharacterSheetEditPage.kt index 9a7b05c..5febf95 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/edit/CharacterSheetEditPage.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/edit/CharacterSheetEditPage.kt @@ -21,6 +21,7 @@ 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.LaunchedEffect import androidx.compose.runtime.Stable import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -32,44 +33,70 @@ import com.pixelized.desktop.lwa.LocalWindowController import com.pixelized.desktop.lwa.composable.decoratedBox.DecoratedBox import com.pixelized.desktop.lwa.navigation.screen.LocalScreenController import com.pixelized.desktop.lwa.navigation.window.LocalWindow +import com.pixelized.desktop.lwa.navigation.window.destination.CharacterSheetCreateWindow +import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.BaseSkillFieldUio +import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.BaseSkillForm import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.FieldUio import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.Form +import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.SkillFieldUio +import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.SkillForm import kotlinx.coroutines.launch import lwacharactersheet.composeapp.generated.resources.Res +import lwacharactersheet.composeapp.generated.resources.character_sheet__skills__common_title +import lwacharactersheet.composeapp.generated.resources.character_sheet__skills__magic_title +import lwacharactersheet.composeapp.generated.resources.character_sheet__skills__special_title import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__add_roll_action +import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__characteristics__title import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__save_action +import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__magic_action +import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__special_action +import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__sub_characteristics__title import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__title import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel -import org.koin.core.annotation.KoinExperimentalAPI @Stable data class CharacterSheetEditPageUio( val id: String, val name: FieldUio, - val skills: List, - val rolls: List, + val strength: FieldUio, + val dexterity: FieldUio, + val constitution: FieldUio, + val height: FieldUio, + val intelligence: FieldUio, + val power: FieldUio, + val charisma: FieldUio, + val movement: FieldUio, + val maxHp: FieldUio, + val maxPP: FieldUio, + val damageBonus: FieldUio, + val armor: FieldUio, + val commonSkills: List, + val specialSkills: List, + val magicSkills: List, + val actions: List, ) { - @Stable - data class SkillGroup( - val title: String, - val type: Type, - val action: String? = null, - val fields: List, - ) { - @Stable - enum class Type { - CHARACTERISTICS, - SUB_CHARACTERISTICS, - SKILLS, - OCCUPATIONS, - MAGICS, - OTHERS, - } - } + val characteristics + get() = listOf( + strength, + dexterity, + constitution, + height, + intelligence, + power, + charisma, + ) + + val subCharacteristics + get() = listOf( + movement, + maxHp, + maxPP, + damageBonus, + armor, + ) } -@OptIn(KoinExperimentalAPI::class) @Composable fun CharacterSheetEditPage( viewModel: CharacterSheetEditViewModel = koinViewModel(), @@ -79,6 +106,10 @@ fun CharacterSheetEditPage( val screen = LocalScreenController.current val scope = rememberCoroutineScope() + LaunchedEffect(Unit) { + window.resize(size = CharacterSheetCreateWindow.size) + } + Surface( modifier = Modifier.fillMaxSize(), ) { @@ -91,8 +122,21 @@ fun CharacterSheetEditPage( null } }, - onNewSkill = viewModel::onSkill, - onNewCategory = viewModel::onNewRoll, + onNewSpecialSkill = { + scope.launch { + viewModel.onNewSpecialSkill() + } + }, + onNewMagicSkill = { + scope.launch { + viewModel.onNewMagicSkill() + } + }, + onNewAction = { + scope.launch { + viewModel.onNewAction() + } + }, onSave = { scope.launch { viewModel.save() @@ -109,8 +153,9 @@ fun CharacterSheetEditPage( fun CharacterSheetEdit( form: CharacterSheetEditPageUio, onBack: (() -> Unit)?, - onNewSkill: (CharacterSheetEditPageUio.SkillGroup) -> Unit, - onNewCategory: () -> Unit, + onNewSpecialSkill: () -> Unit, + onNewMagicSkill: () -> Unit, + onNewAction: () -> Unit, onSave: () -> Unit, ) { Scaffold( @@ -142,7 +187,7 @@ fun CharacterSheetEdit( modifier = Modifier .verticalScroll(state = rememberScrollState()) .padding(paddingValues = paddingValues) - .padding(all = 24.dp), + .padding(all = 16.dp), verticalArrangement = Arrangement.spacedBy(space = 16.dp) ) { Form( @@ -150,56 +195,161 @@ fun CharacterSheetEdit( field = form.name, ) - form.skills.forEach { - DecoratedBox( - modifier = Modifier.animateContentSize(), + DecoratedBox( + modifier = Modifier.animateContentSize(), + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text( - modifier = Modifier.padding(vertical = 8.dp), - style = MaterialTheme.typography.caption, - text = it.title, + Text( + modifier = Modifier.padding(vertical = 8.dp), + overflow = TextOverflow.Ellipsis, + maxLines = 1, + style = MaterialTheme.typography.caption, + text = stringResource(Res.string.character_sheet_edit__characteristics__title), + ) + form.characteristics.forEach { + Form( + modifier = Modifier.fillMaxWidth(), + field = it, ) - it.fields.forEach { - Form( - modifier = Modifier.fillMaxWidth(), - field = it, + } + } + } + + DecoratedBox( + modifier = Modifier.animateContentSize(), + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + modifier = Modifier.padding(vertical = 8.dp), + overflow = TextOverflow.Ellipsis, + maxLines = 1, + style = MaterialTheme.typography.caption, + text = stringResource(Res.string.character_sheet_edit__sub_characteristics__title), + ) + form.subCharacteristics.forEach { + Form( + modifier = Modifier.fillMaxWidth(), + field = it, + ) + } + } + } + + DecoratedBox( + modifier = Modifier.animateContentSize(), + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + modifier = Modifier.padding(vertical = 8.dp), + overflow = TextOverflow.Ellipsis, + maxLines = 1, + style = MaterialTheme.typography.caption, + text = stringResource(Res.string.character_sheet__skills__common_title), + ) + form.commonSkills.forEach { + BaseSkillForm( + modifier = Modifier + .fillMaxWidth() + .padding(end = 4.dp), + field = it, + ) + } + } + } + + DecoratedBox( + modifier = Modifier.animateContentSize(), + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + modifier = Modifier.padding(vertical = 8.dp), + overflow = TextOverflow.Ellipsis, + maxLines = 1, + style = MaterialTheme.typography.caption, + text = stringResource(Res.string.character_sheet__skills__special_title), + ) + form.specialSkills.forEach { + SkillForm( + modifier = Modifier + .fillMaxWidth() + .padding(end = 4.dp), + field = it, + ) + } + TextButton( + modifier = Modifier.align(alignment = Alignment.End), + onClick = onNewSpecialSkill, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(space = 4.dp), + ) { + Text( + overflow = TextOverflow.Ellipsis, + maxLines = 1, + text = stringResource(Res.string.character_sheet_edit__skills__special_action), + ) + Icon( + imageVector = Icons.Default.Add, + contentDescription = null, ) - } - it.action?.let { label -> - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy( - space = 8.dp, - alignment = Alignment.End - ) - ) { - TextButton( - onClick = { onNewSkill(it) }, - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(space = 4.dp), - ) { - Text( - text = label, - ) - Icon( - imageVector = Icons.Default.Add, - contentDescription = null, - ) - } - } - } } } } } - form.rolls.forEach { + DecoratedBox( + modifier = Modifier.animateContentSize(), + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + modifier = Modifier.padding(vertical = 8.dp), + overflow = TextOverflow.Ellipsis, + maxLines = 1, + style = MaterialTheme.typography.caption, + text = stringResource(Res.string.character_sheet__skills__magic_title), + ) + form.magicSkills.forEach { + SkillForm( + modifier = Modifier + .fillMaxWidth() + .padding(end = 4.dp), + field = it, + ) + } + TextButton( + modifier = Modifier.align(alignment = Alignment.End), + onClick = onNewMagicSkill, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(space = 4.dp), + ) { + Text( + overflow = TextOverflow.Ellipsis, + maxLines = 1, + text = stringResource(Res.string.character_sheet_edit__skills__magic_action), + ) + Icon( + imageVector = Icons.Default.Add, + contentDescription = null, + ) + } + } + } + } + + form.actions.forEach { Form( modifier = Modifier.fillMaxWidth(), valueWidth = 120.dp, @@ -212,9 +362,13 @@ fun CharacterSheetEdit( horizontalArrangement = Arrangement.End, ) { TextButton( - onClick = onNewCategory, + onClick = onNewAction, ) { - Text(text = stringResource(Res.string.character_sheet_edit__add_roll_action)) + Text( + overflow = TextOverflow.Ellipsis, + maxLines = 1, + text = stringResource(Res.string.character_sheet_edit__add_roll_action) + ) } } @@ -225,7 +379,11 @@ fun CharacterSheetEdit( TextButton( onClick = onSave, ) { - Text(text = stringResource(Res.string.character_sheet_edit__save_action)) + Text( + overflow = TextOverflow.Ellipsis, + maxLines = 1, + text = stringResource(Res.string.character_sheet_edit__save_action), + ) } } } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/edit/CharacterSheetEditViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/edit/CharacterSheetEditViewModel.kt index 359d3bc..87029cc 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/edit/CharacterSheetEditViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/edit/CharacterSheetEditViewModel.kt @@ -6,78 +6,106 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import com.pixelized.desktop.lwa.navigation.screen.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.common.SkillFieldFactory import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.FieldUio import kotlinx.coroutines.runBlocking +import lwacharactersheet.composeapp.generated.resources.Res +import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__magic_title +import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__special_title +import org.jetbrains.compose.resources.getString +import java.util.UUID class CharacterSheetEditViewModel( private val characterSheetRepository: CharacterSheetRepository, + private val sheetFactory: CharacterSheetEditFactory, + private val skillFactory: SkillFieldFactory, savedStateHandle: SavedStateHandle, ) : ViewModel() { private val argument = CharacterSheetEditDestination.Argument(savedStateHandle) - private val factory = CharacterSheetEditFactory() val enableBack = argument.enableBack private val _characterSheet = mutableStateOf( characterSheetRepository.characterSheetFlow(id = argument.id).value.let { - runBlocking { factory.convertToUio(it) } + runBlocking { + sheetFactory.convertToUio( + sheet = it, + onDeleteSkill = ::deleteSkill, + ) + } } ) val characterSheet: State 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().apply { - addAll(group.fields) - add( - FieldUio.create( - isLabelEditable = true, - initialLabel = "", - 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.OTHERS -> "" - } - }, - ) - ) - } - ) - } else { - group - } - } + suspend fun onNewSpecialSkill() { + val id = UUID.randomUUID().toString() + val skill = skillFactory.createSkill( + id = id, + label = getString(Res.string.character_sheet_edit__skills__special_title), + options = listOf( + skillFactory.occupationOption(checked = false), + skillFactory.deleteOption { deleteSkill(skillId = id) } + ), + ) + val skills = _characterSheet.value.specialSkills + .toMutableList() + .also { it.add(skill) } + _characterSheet.value = characterSheet.value.copy( + specialSkills = skills, ) } - fun onNewRoll() { - val sheet = _characterSheet.value - _characterSheet.value = sheet.copy( - rolls = sheet.rolls.toMutableList().apply { - add( - FieldUio.create( - initialLabel = "", - isLabelEditable = true, - valuePlaceHolder = { "" }, - ) - ) - } + suspend fun onNewMagicSkill() { + val id = UUID.randomUUID().toString() + val skill = skillFactory.createSkill( + id = id, + label = getString(Res.string.character_sheet_edit__skills__magic_title), + options = listOf( + skillFactory.occupationOption(checked = false), + skillFactory.deleteOption { deleteSkill(skillId = id) } + ), + ) + val skills = _characterSheet.value.magicSkills + .toMutableList() + .also { it.add(skill) } + _characterSheet.value = characterSheet.value.copy( + magicSkills = skills, + ) + } + + suspend fun onNewAction() { + val field = FieldUio.create( + initialLabel = "", + isLabelEditable = true, + valuePlaceHolder = { "" }, + ) + val actions = _characterSheet.value.actions.toMutableList().also { + it.add(field) + } + _characterSheet.value = _characterSheet.value.copy( + actions = actions, + ) + } + + private fun deleteSkill(skillId: String) { + _characterSheet.value = _characterSheet.value.copy( + specialSkills = _characterSheet.value.specialSkills.toMutableList().also { skills -> + skills.removeIf { it.id == skillId } + }, + magicSkills = _characterSheet.value.magicSkills.toMutableList().also { skills -> + skills.removeIf { it.id == skillId } + }, ) } suspend fun save() { - val sheet = _characterSheet.value - val model = factory.convertToModel(sheet = sheet) - characterSheetRepository.save(characterSheet = model) + val updatedSheet = sheetFactory.updateCharacterSheet( + currentSheet = characterSheetRepository.characterSheetFlow(id = _characterSheet.value.id).value, + editedSheet = _characterSheet.value, + ) + characterSheetRepository.save( + characterSheet = updatedSheet, + ) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/edit/composable/BaseSkillField.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/edit/composable/BaseSkillField.kt new file mode 100644 index 0000000..748c69e --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/edit/composable/BaseSkillField.kt @@ -0,0 +1,117 @@ +package com.pixelized.desktop.lwa.screen.characterSheet.edit.composable + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material.ContentAlpha +import androidx.compose.material.DropdownMenu +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.option.CheckedOption +import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.option.DropDownCheckedMenuItem +import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.textfield.TextFieldWrapper +import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.textfield.TextFieldWrapperUio +import lwacharactersheet.composeapp.generated.resources.Res +import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__base_label +import org.jetbrains.compose.resources.stringResource + +@Stable +class BaseSkillFieldUio( + val id: String, + val label: String, + val base: State, + val bonus: TextFieldWrapperUio, + val level: TextFieldWrapperUio, + val option: CheckedOption, +) + +@Composable +fun BaseSkillForm( + modifier: Modifier = Modifier, + field: BaseSkillFieldUio, +) { + val showMenu = remember { mutableStateOf(false) } + + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(space = 4.dp, alignment = Alignment.End), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + modifier = Modifier + .weight(1f) + .padding(start = 16.dp, bottom = 12.dp) + .align(alignment = Alignment.Bottom), + style = MaterialTheme.typography.body1, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + text = field.label, + ) + Column( + modifier = Modifier + .width(width = 96.dp) + .padding(start = 16.dp), + ) { + Text( + style = MaterialTheme.typography.caption, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + color = MaterialTheme.colors.onSurface.copy(ContentAlpha.medium), + text = stringResource(Res.string.character_sheet_edit__skills__base_label), + ) + Text( + style = MaterialTheme.typography.body1, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + text = "+${field.base.value}", + ) + } + TextFieldWrapper( + modifier = Modifier.width(width = 96.dp), + wrapper = field.bonus, + ) + TextFieldWrapper( + modifier = Modifier.width(width = 96.dp), + wrapper = field.level, + ) + Box { + IconButton( + onClick = { showMenu.value = showMenu.value.not() }, + ) { + Icon( + imageVector = Icons.Default.MoreVert, + tint = MaterialTheme.colors.primary, + contentDescription = null, + ) + } + DropdownMenu( + expanded = showMenu.value, + onDismissRequest = { showMenu.value = false } + ) { + DropDownCheckedMenuItem( + wrapper = field.option, + onClick = { + showMenu.value = false + field.option.onOption() + } + ) + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/edit/composable/FieldUio.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/edit/composable/Field.kt similarity index 88% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/edit/composable/FieldUio.kt rename to composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/edit/composable/Field.kt index cfe360c..fdaf53e 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/edit/composable/FieldUio.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/edit/composable/Field.kt @@ -4,7 +4,9 @@ import androidx.compose.animation.AnimatedContent import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.togetherWith +import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width @@ -24,7 +26,9 @@ import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import com.pixelized.desktop.lwa.utils.preview.ContentPreview +@Deprecated("redo") @Stable open class FieldUio( val isLabelDisplayed: Boolean, @@ -127,4 +131,23 @@ fun Form( } } } +} + +@Composable +@Preview +private fun Preview() { + ContentPreview( + paddingValues = PaddingValues(all = 16.dp) + ) { + Form( + field = FieldUio.create( + isLabelDisplayed = true, + isLabelEditable = true, + initialLabel = "label", + labelPlaceHolder = { "labelPlaceholder" }, + initialValue = "value", + valuePlaceHolder = { "valuePlaceholder" }, + ) + ) + } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/edit/composable/SpecialSkillField.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/edit/composable/SpecialSkillField.kt new file mode 100644 index 0000000..9dbc3fa --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/edit/composable/SpecialSkillField.kt @@ -0,0 +1,257 @@ +package com.pixelized.desktop.lwa.screen.characterSheet.edit.composable + +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.width +import androidx.compose.material.DropdownMenu +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.singleWindowApplication +import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.textfield.TextFieldWrapperUio +import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.textfield.TextFieldWrapper +import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.option.ActionOption +import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.option.CheckedOption +import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.option.DropDownMenuItemWrapper +import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.option.OptionUio +import com.pixelized.desktop.lwa.utils.preview.ContentPreview +import java.util.UUID + +@Stable +class SkillFieldUio( + val id: String, + val label: TextFieldWrapperUio, + val base: TextFieldWrapperUio, + val bonus: TextFieldWrapperUio, + val level: TextFieldWrapperUio, + val options: List, +) + +@Composable +fun SkillForm( + modifier: Modifier = Modifier, + field: SkillFieldUio, +) { + val showMenu = remember { mutableStateOf(false) } + + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(space = 4.dp, alignment = Alignment.End), + verticalAlignment = Alignment.CenterVertically, + ) { + TextFieldWrapper( + modifier = Modifier.weight(1f), + wrapper = field.label, + ) + TextFieldWrapper( + modifier = Modifier.width(width = 96.dp), + wrapper = field.base, + ) + TextFieldWrapper( + modifier = Modifier.width(width = 96.dp), + wrapper = field.bonus, + ) + TextFieldWrapper( + modifier = Modifier.width(width = 96.dp), + wrapper = field.level, + ) + if (field.options.isNotEmpty()) { + Box { + IconButton( + onClick = { showMenu.value = showMenu.value.not() }, + ) { + Icon( + imageVector = Icons.Default.MoreVert, + tint = MaterialTheme.colors.primary, + contentDescription = null, + ) + } + DropdownMenu( + expanded = showMenu.value, + onDismissRequest = { showMenu.value = false } + ) { + field.options.forEach { + DropDownMenuItemWrapper( + wrapper = it, + onClick = { + showMenu.value = false + it.onOption() + } + ) + } + } + } + } + } +} + +// PREVIEW + +@Composable +@Preview +private fun Preview() { + ContentPreview( + modifier = Modifier.width(width = 1000.dp), + paddingValues = PaddingValues(all = 16.dp), + ) { + SkillForm( + field = skillFieldUio( + labelLabel = "compétence", + labelValue = "Bagarre", + baseLabel = "Base", + baseValue = "Stat(DEX*2)", + bonusLabel = "Bonus", + bonusValue = "50", + levelLabel = "Niveau", + levelValue = "5", + ), + ) + } +} + +fun main() = singleWindowApplication(title = "Context menu") { + ContentPreview( + modifier = Modifier.width(1000.dp), + paddingValues = PaddingValues(all = 16.dp), + ) { + Column( + verticalArrangement = Arrangement.spacedBy(space = 8.dp) + ) { + SkillForm( + field = skillFieldUio( + labelLabel = "compétence", + labelValue = "Bagarre", + baseLabel = "Base", + baseValue = "Stat(DEX*2)", + bonusLabel = "Bonus", + bonusValue = "55", + levelLabel = "Niveau", + levelValue = "0", + options = listOf( + CheckedOption.OccupationOption( + label = "Base minimum 40", + checked = mutableStateOf(true), + onOption = { }, + ), + ActionOption.DeleteOptionUio( + icon = Icons.Default.Delete, + label = "Supprimer", + onOption = { }, + ), + ) + ), + ) + SkillForm( + field = skillFieldUio( + labelLabel = "compétence", + labelValue = "Esquive", + baseLabel = "Base", + baseValue = "Stat(DEX*2)", + bonusLabel = "Bonus", + bonusValue = "40", + levelLabel = "Niveau", + levelValue = "0", + options = listOf( + CheckedOption.OccupationOption( + label = "Base minimum 40", + checked = mutableStateOf(true), + onOption = { }, + ), + ActionOption.DeleteOptionUio( + icon = Icons.Default.Delete, + label = "Supprimer", + onOption = { }, + ), + ) + ), + ) + SkillForm( + field = skillFieldUio( + labelLabel = "compétence", + labelValue = "Saisie", + baseLabel = "Base", + baseValue = "Stat(FOR+TAI)", + bonusLabel = "Bonus", + bonusValue = "0", + levelLabel = "Niveau", + levelValue = "0", + options = listOf( + CheckedOption.OccupationOption( + label = "Base minimum 40", + checked = mutableStateOf(true), + onOption = { }, + ), + ActionOption.DeleteOptionUio( + icon = Icons.Default.Delete, + label = "Supprimer", + onOption = { }, + ), + ) + ), + ) + } + } +} + +private fun skillFieldUio( + labelLabel: String, + labelValue: String, + baseLabel: String, + baseValue: String, + bonusLabel: String, + bonusValue: String, + levelLabel: String, + levelValue: String, + options: List = emptyList(), +): SkillFieldUio { + return SkillFieldUio( + id = UUID.randomUUID().toString(), + label = mutableStateOf(labelValue).let { state -> + TextFieldWrapperUio( + enable = true, + label = labelLabel, + value = state, + onValueChange = { state.value = it }, + ) + }, + base = mutableStateOf(baseValue).let { state -> + TextFieldWrapperUio( + enable = true, + label = baseLabel, + value = state, + onValueChange = { state.value = it }, + ) + }, + bonus = mutableStateOf(bonusValue).let { state -> + TextFieldWrapperUio( + enable = true, + label = bonusLabel, + value = state, + onValueChange = { state.value = it }, + ) + }, + level = mutableStateOf(levelValue).let { state -> + TextFieldWrapperUio( + enable = true, + label = levelLabel, + value = state, + onValueChange = { state.value = it }, + ) + }, + options = options, + ) +} diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/edit/composable/option/ActionOption.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/edit/composable/option/ActionOption.kt new file mode 100644 index 0000000..7b0383c --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/edit/composable/option/ActionOption.kt @@ -0,0 +1,45 @@ +package com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.option + +import androidx.compose.foundation.layout.padding +import androidx.compose.material.DropdownMenuItem +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.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp + +@Stable +sealed class ActionOption( + val icon: ImageVector, + val label: String, + onOption: () -> Unit, +) : OptionUio(onOption = onOption) { + + class DeleteOptionUio(icon: ImageVector, label: String, onOption: () -> Unit) : + ActionOption(icon = icon, label = label, onOption = onOption) +} + +@Composable +fun DropDownActionMenuItem( + modifier: Modifier = Modifier, + wrapper: ActionOption, + onClick: () -> Unit, +) { + DropdownMenuItem( + modifier = modifier, + onClick = onClick, + ) { + Icon( + tint = MaterialTheme.colors.primary, + imageVector = wrapper.icon, + contentDescription = null, + ) + Text( + modifier = Modifier.padding(start = 8.dp), + text = wrapper.label, + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/edit/composable/option/CheckedOption.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/edit/composable/option/CheckedOption.kt new file mode 100644 index 0000000..55c6dbd --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/edit/composable/option/CheckedOption.kt @@ -0,0 +1,49 @@ +package com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.option + +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.Checkbox +import androidx.compose.material.CheckboxDefaults +import androidx.compose.material.DropdownMenuItem +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.State +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Stable +sealed class CheckedOption( + val label: String, + val checked: State, + onOption: () -> Unit, +) : OptionUio(onOption = onOption) { + class OccupationOption(label: String, checked: State, onOption: () -> Unit) : + CheckedOption(label = label, checked = checked, onOption = onOption) +} + +@Composable +fun DropDownCheckedMenuItem( + modifier: Modifier = Modifier, + wrapper: CheckedOption, + onClick: () -> Unit, +) { + DropdownMenuItem( + modifier = modifier, + onClick = onClick, + ) { + Checkbox( + modifier = Modifier.size(size = 24.dp), + checked = wrapper.checked.value, + colors = CheckboxDefaults.colors( + checkedColor = MaterialTheme.colors.primary, + ), + onCheckedChange = null + ) + Text( + modifier = Modifier.padding(start = 8.dp), + text = wrapper.label, + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/edit/composable/option/Option.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/edit/composable/option/Option.kt new file mode 100644 index 0000000..2c66b12 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/edit/composable/option/Option.kt @@ -0,0 +1,31 @@ +package com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.option + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.ui.Modifier + +@Stable +sealed class OptionUio( + val onOption: () -> Unit, +) + +@Composable +fun DropDownMenuItemWrapper( + modifier: Modifier = Modifier, + wrapper: OptionUio, + onClick: () -> Unit, +) { + when (wrapper) { + is ActionOption -> DropDownActionMenuItem( + modifier = modifier, + wrapper = wrapper, + onClick = onClick, + ) + + is CheckedOption -> DropDownCheckedMenuItem( + modifier = modifier, + wrapper = wrapper, + onClick = onClick, + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/edit/composable/textfield/FormWrapper.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/edit/composable/textfield/FormWrapper.kt new file mode 100644 index 0000000..f7cd5be --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/edit/composable/textfield/FormWrapper.kt @@ -0,0 +1,51 @@ +package com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.textfield + +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.TextField +import androidx.compose.material.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.State +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 com.pixelized.desktop.lwa.utils.rememberKeyboardActions + +@Stable +data class TextFieldWrapperUio( + val enable: Boolean, + val label: String, + val value: State, + val onValueChange: (String) -> Unit, +) + +@Composable +fun TextFieldWrapper( + modifier: Modifier = Modifier, + wrapper: TextFieldWrapperUio, +) { + val focus = LocalFocusManager.current + + TextField( + modifier = modifier, + colors = TextFieldDefaults.textFieldColors( + backgroundColor = MaterialTheme.colors.onSurface.copy(alpha = 0.03f), + ), + keyboardActions = rememberKeyboardActions { + focus.moveFocus(FocusDirection.Next) + }, + enabled = wrapper.enable, + singleLine = true, + label = { + Text( + overflow = TextOverflow.Ellipsis, + maxLines = 1, + text = wrapper.label + ) + }, + onValueChange = { wrapper.onValueChange(it) }, + value = wrapper.value.value, + ) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/main/MainPage.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/main/MainPage.kt index 99a7a72..f5f4c09 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/main/MainPage.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/main/MainPage.kt @@ -31,7 +31,6 @@ import lwacharactersheet.composeapp.generated.resources.main_page__network_actio import lwacharactersheet.composeapp.generated.resources.main_page__roll_history_action import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel -import org.koin.core.annotation.KoinExperimentalAPI @Stable data class CharacterUio( @@ -39,7 +38,6 @@ data class CharacterUio( val name: String, ) -@OptIn(KoinExperimentalAPI::class) @Composable fun MainPage( viewModel: MainPageViewModel = koinViewModel(), @@ -54,7 +52,7 @@ fun MainPage( modifier = Modifier .verticalScroll(state = rememberScrollState()) .fillMaxSize() - .padding(horizontal = 24.dp), + .padding(horizontal = 16.dp), contentAlignment = Alignment.Center, ) { MainPageContent( diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/roll/RollPage.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/roll/RollPage.kt index 69fced2..038957d 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/roll/RollPage.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/roll/RollPage.kt @@ -44,7 +44,6 @@ 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 com.pixelized.desktop.lwa.screen.roll.DifficultyUio.Difficulty import com.pixelized.desktop.lwa.utils.DisableInteractionSource import kotlinx.coroutines.launch @@ -60,7 +59,6 @@ import lwacharactersheet.composeapp.generated.resources.roll_page__roll__success import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel -import org.koin.core.annotation.KoinExperimentalAPI @Stable data class RollTitleUio( @@ -84,7 +82,6 @@ data class DifficultyUio( } } -@OptIn(KoinExperimentalAPI::class) @Composable fun RollPage( viewModel: RollViewModel = koinViewModel(), @@ -131,6 +128,10 @@ fun RollPage( horizontalAlignment = Alignment.CenterHorizontally, ) { Box( + modifier = Modifier.graphicsLayer { + this.scaleX = viewModel.rollScale.value + this.scaleY = viewModel.rollScale.value + }, contentAlignment = Alignment.Center ) { Icon( @@ -150,8 +151,8 @@ fun RollPage( AnimatedContent( targetState = viewModel.rollResult.value?.value?.toString() ?: "", transitionSpec = { - val enter = fadeIn() + slideInVertically { 32 } - val exit = fadeOut() + slideOutVertically { -32 } + val enter = fadeIn() + slideInVertically { -32 } + val exit = fadeOut() + slideOutVertically { 32 } enter togetherWith exit using SizeTransform(clip = false) }, ) { label -> diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/roll/RollViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/roll/RollViewModel.kt index 31136a1..45d3314 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/roll/RollViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/screen/roll/RollViewModel.kt @@ -3,6 +3,7 @@ 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.animation.core.tween import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel @@ -48,6 +49,7 @@ class RollViewModel( val rollResult: State get() = _rollResult val rollRotation = Animatable(0f) + val rollScale = Animatable(1f) private val _rollDifficulty = mutableStateOf(null) val rollDifficulty: State get() = _rollDifficulty @@ -95,7 +97,10 @@ class RollViewModel( rollAction: String, rollSuccessValue: Int?, ) { - runBlocking { rollRotation.snapTo(0f) } + runBlocking { + rollRotation.snapTo(0f) + rollScale.snapTo(1f) + } this.sheet = characterSheetRepository.characterSheetFlow(id = sheet.id).value!! this.rollAction = rollAction @@ -124,6 +129,22 @@ class RollViewModel( rollJob?.cancel() rollJob = launch { + launch { + rollScale.animateTo( + targetValue = 1.20f, + animationSpec = spring( + dampingRatio = Spring.DampingRatioNoBouncy, + stiffness = 800f, + ) + ) + rollScale.animateTo( + targetValue = 1f, + animationSpec = spring( + dampingRatio = 0.28f, + stiffness = 800f, + ) + ) + } launch { rollRotation.animateTo( targetValue = rollRotation.value.let { it - it % 360 } + 360f * 3, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/utils/preview/ContentPreview.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/utils/preview/ContentPreview.kt new file mode 100644 index 0000000..45b89c0 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/utils/preview/ContentPreview.kt @@ -0,0 +1,31 @@ +package com.pixelized.desktop.lwa.utils.preview + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import com.pixelized.desktop.lwa.theme.LwaTheme + +@Composable +fun ContentPreview( + modifier: Modifier = Modifier, + paddingValues: PaddingValues = PaddingValues(), + content: @Composable () -> Unit, +) { + LwaTheme { + Surface { + Box( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues = paddingValues) + .then(other = modifier), + contentAlignment = Alignment.Center, + content = { content() }, + ) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/utils/rememberKeyboardActions.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/utils/rememberKeyboardActions.kt new file mode 100644 index 0000000..b09b316 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/utils/rememberKeyboardActions.kt @@ -0,0 +1,9 @@ +package com.pixelized.desktop.lwa.utils + +import androidx.compose.foundation.text.KeyboardActionScope +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember + +@Composable +fun rememberKeyboardActions(onAny: KeyboardActionScope.() -> Unit) = remember { KeyboardActions(onAny = onAny) } \ No newline at end of file diff --git a/composeApp/src/commonTest/kotlin/com/pixelized/desktop/lwa/parser/ArithmeticParserTest.kt b/composeApp/src/commonTest/kotlin/com/pixelized/desktop/lwa/parser/ArithmeticParserTest.kt new file mode 100644 index 0000000..34729f2 --- /dev/null +++ b/composeApp/src/commonTest/kotlin/com/pixelized/desktop/lwa/parser/ArithmeticParserTest.kt @@ -0,0 +1,156 @@ +package com.pixelized.desktop.lwa.parser + +import com.pixelized.desktop.lwa.parser.arithmetic.Arithmetic +import com.pixelized.desktop.lwa.parser.arithmetic.ArithmeticParser +import com.pixelized.desktop.lwa.parser.arithmetic.Instruction +import kotlin.test.Test +import kotlin.test.assertFails + +class ArithmeticParserTest { + + @Test + fun testDiceInstructionParse() { + val parser = ArithmeticParser() + + fun test( + instruction: String, + expectedModifier: Instruction.Dice.Modifier?, + expectedQuantity: Int, + expectedFaces: Int, + ) { + val dice = parser.parseInstruction(instruction = instruction) + + assert(dice is Instruction.Dice) { + "Instruction should be ArithmeticInstruction.Dice but was: ${dice::class.java.simpleName}" + } + (dice as? Instruction.Dice)?.let { + assert(dice.modifier == expectedModifier) { + "$instruction modifier should be:\"$expectedModifier\", but was: ${dice.modifier}" + } + assert(dice.quantity == expectedQuantity) { + "$instruction quantity should be \"$expectedQuantity\" but was ${dice.quantity}" + } + assert(dice.faces == expectedFaces) { + "$instruction faces should be \"$expectedFaces\" but was ${dice.faces}" + } + } + } + + test( + instruction = "1d100", + expectedModifier = null, + expectedQuantity = 1, + expectedFaces = 100, + ) + test( + instruction = "a2d6", + expectedModifier = Instruction.Dice.Modifier.ADVANTAGE, + expectedQuantity = 2, + expectedFaces = 6, + ) + test( + instruction = "d1d2", + expectedModifier = Instruction.Dice.Modifier.DISADVANTAGE, + expectedQuantity = 1, + expectedFaces = 2, + ) + test( + instruction = "e6d6", + expectedModifier = Instruction.Dice.Modifier.EMPHASIS, + expectedQuantity = 6, + expectedFaces = 6, + ) + } + + @Test + fun testWordInstructionParse() { + val parser = ArithmeticParser() + + ArithmeticParser.words.map { instruction -> + val word = parser.parseInstruction(instruction = instruction) + + assert(word is Instruction.Word) { + "Instruction should be ArithmeticInstruction.Word but was: ${word::class.java.simpleName}" + } + (word as? Instruction.Word)?.let { + assert(it.name == instruction) { + "Instruction should be $instruction, but was ${it.name}" + } + } + } + } + + @Test + fun testFlatInstructionParse() { + val parser = ArithmeticParser() + + "100".let { instruction -> + val flat = parser.parseInstruction(instruction = instruction) + + assert(flat is Instruction.Flat) { + "Instruction should be ArithmeticInstruction.Flat but was: ${flat::class.java.simpleName}" + } + (flat as? Instruction.Flat)?.let { + assert("${it.value}" == instruction) { + "Instruction should be $instruction, but was ${it.value}" + } + } + } + } + + @Test + fun testFailedInstructionParse() { + val parser = ArithmeticParser() + assertFails( + message = "Instruction parse should failed with UnknownInstruction", + ) { + parser.parseInstruction(instruction = "a110") + } + } + + @Test + fun testRollParse() { + val parser = ArithmeticParser() + + fun test( + arithmetics: Arithmetic, + expectedSign: Int, + expectedInstruction: Instruction, + ) { + assert(arithmetics.sign == expectedSign) { + "Arithmetic sign should be $expectedSign but was: ${arithmetics.sign}" + } + assert(arithmetics.instruction == expectedInstruction) { + "Arithmetic instruction should be $expectedInstruction but was: ${arithmetics.instruction}" + } + } + + val instructions = parser.parse(value = "1+1d6+2-BDC+BDD") + + test( + arithmetics = instructions[0], + expectedSign = 1, + expectedInstruction = Instruction.Flat(value = 1), + ) + test( + arithmetics = instructions[1], + expectedSign = 1, + expectedInstruction = Instruction.Dice(quantity = 1, faces = 6, modifier = null), + ) + test( + arithmetics = instructions[2], + expectedSign = 1, + expectedInstruction = Instruction.Flat(value = 2), + ) + test( + arithmetics = instructions[3], + expectedSign = -1, + expectedInstruction = Instruction.Word.BDC, + ) + test( + arithmetics = instructions[4], + expectedSign = 1, + expectedInstruction = Instruction.Word.BDD, + ) + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d000d57..3cf23e9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,9 +15,12 @@ composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "k kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } + [libraries] kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } +compose-desktop-preview = { group = "org.jetbrains.compose.ui", name = "ui-tooling-preview", version.ref = "compose-multiplatform" } + androidx-lifecycle-viewmodel = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "androidx-lifecycle" } androidx-lifecycle-runtime-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidx-lifecycle" } androidx-navigation-compose = { group = "org.jetbrains.androidx.navigation", name = "navigation-compose", version.ref = "androidx-navigation" }