diff --git a/composeApp/src/commonMain/composeResources/drawable/compose-multiplatform.xml b/composeApp/src/commonMain/composeResources/drawable/compose-multiplatform.xml deleted file mode 100644 index c0bcfb2..0000000 --- a/composeApp/src/commonMain/composeResources/drawable/compose-multiplatform.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_d20_32dp.xml b/composeApp/src/commonMain/composeResources/drawable/ic_d20_32dp.xml new file mode 100644 index 0000000..76d9633 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/ic_d20_32dp.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/composeApp/src/desktopMain/kotlin/com/pixelized/desktop/lwa/App.kt b/composeApp/src/desktopMain/kotlin/com/pixelized/desktop/lwa/App.kt index 90da7b4..3abdeb6 100644 --- a/composeApp/src/desktopMain/kotlin/com/pixelized/desktop/lwa/App.kt +++ b/composeApp/src/desktopMain/kotlin/com/pixelized/desktop/lwa/App.kt @@ -1,21 +1,24 @@ package com.pixelized.desktop.lwa import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.Button +import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Window import androidx.compose.ui.window.rememberWindowState -import com.pixelized.desktop.lwa.screen.CharacterSheet -import com.pixelized.desktop.lwa.screen.CharacterSheetUio +import androidx.lifecycle.viewmodel.compose.viewModel +import com.pixelized.desktop.lwa.screen.characterSheet.CharacterSheet +import com.pixelized.desktop.lwa.screen.characterSheet.CharacterSheetViewModel +import com.pixelized.desktop.lwa.screen.overlay.BlurOverlay +import com.pixelized.desktop.lwa.screen.overlay.BlurOverlayViewModel +import com.pixelized.desktop.lwa.screen.roll.RollPage +import com.pixelized.desktop.lwa.screen.roll.RollViewModel import com.pixelized.desktop.lwa.theme.LwaTheme import org.jetbrains.compose.ui.tooling.preview.Preview @@ -23,31 +26,61 @@ import org.jetbrains.compose.ui.tooling.preview.Preview @Preview fun App() { LwaTheme { - val characterSheet = remember { mutableStateOf(null) } - - Column( - modifier = Modifier.padding(all = 16.dp), + Surface( + modifier = Modifier.fillMaxSize() ) { - Button( - onClick = { - characterSheet.value = CharacterSheetUio.Koryas - } - ) { - Text(text = "Koryas Tissenpa") - } - } + val sheetViewModel = viewModel { CharacterSheetViewModel() } + val overlayViewModel = viewModel { BlurOverlayViewModel() } + val rollViewModel = viewModel { RollViewModel() } - characterSheet.value?.let { sheet -> - Window( - onCloseRequest = { characterSheet.value = null }, -// state = rememberWindowState(size = DpSize(width = 320.dp + 32.dp, height = 800.dp)), - title = "LwaCharacterSheet", + Column( + modifier = Modifier.padding(all = 16.dp), ) { - CharacterSheet( - modifier = Modifier.fillMaxWidth(), - width = 320.dp, - characterSheet = sheet - ) + Button( + onClick = sheetViewModel::showCharacterSheet, + ) { + Text(text = "Koryas Tissenpa") + } + } + + sheetViewModel.sheet.value?.let { sheet -> + Window( + onCloseRequest = sheetViewModel::hideCharacterSheet, + state = rememberWindowState( + width = 320.dp + 64.dp, + height = 900.dp, + ), + title = "LwaCharacterSheet", + ) { + Surface( + modifier = Modifier.fillMaxSize() + ) { + BlurOverlay( + viewModel = overlayViewModel, + overlay = { + RollPage( + viewModel = rollViewModel, + onDismissRequest = overlayViewModel::hide, + ) + }, + content = { + CharacterSheet( + modifier = Modifier.fillMaxWidth(), + width = 320.dp, + characterSheet = sheet, + onCharacteristic = { characteristic -> + rollViewModel.prepareRoll(characteristic = characteristic) + overlayViewModel.show() + }, + onSkill = { node -> + rollViewModel.prepareRoll(node = node) + overlayViewModel.show() + } + ) + }, + ) + } + } } } } diff --git a/composeApp/src/desktopMain/kotlin/com/pixelized/desktop/lwa/business/RollUseCase.kt b/composeApp/src/desktopMain/kotlin/com/pixelized/desktop/lwa/business/RollUseCase.kt new file mode 100644 index 0000000..59b8e34 --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/com/pixelized/desktop/lwa/business/RollUseCase.kt @@ -0,0 +1,14 @@ +package com.pixelized.desktop.lwa.business + +object RollUseCase { + private val d100 = (1..100) + + /** + * (Math.random() * 100 + 1).toInt() + * TODO : test. + */ + fun rollD100(): Int { + return d100.random() + } + +} \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/com/pixelized/desktop/lwa/business/SkillStepUseCase.kt b/composeApp/src/desktopMain/kotlin/com/pixelized/desktop/lwa/business/SkillStepUseCase.kt new file mode 100644 index 0000000..b60c256 --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/com/pixelized/desktop/lwa/business/SkillStepUseCase.kt @@ -0,0 +1,95 @@ +package com.pixelized.desktop.lwa.business + +import kotlin.math.max +import kotlin.math.min + +object SkillStepUseCase { + + data class SkillStep( + val criticalSuccessRange: IntRange, + val specialSuccessRange: IntRange, + val successRange: IntRange, + val failureRange: IntRange, + val criticalFailureRange: IntRange, + ) { + constructor( + criticalSuccess: Pair?, + specialSuccess: Pair, + success: Pair, + failure: Pair?, + criticalFailure: Pair, + ) : this( + criticalSuccessRange = criticalSuccess + ?.let { IntRange(it.first, it.second) } + ?: IntRange(-1, -1), + specialSuccessRange = specialSuccess + .let { IntRange(it.first, it.second) }, + successRange = success + .let { IntRange(it.first, it.second) }, + failureRange = failure + ?.let { IntRange(it.first, it.second) } ?: IntRange(-1, -1), + criticalFailureRange = criticalFailure + .let { IntRange(it.first, it.second) }, + ) + } + + /** + * Helper method to compute the range in which a roll is a either critical, special, success or failure. + * TODO : test. + */ + fun computeSkillStep(skill: Int): SkillStep { + val criticalSuccess: Pair? = when (skill) { + in (0..5) -> null + in (10..25) -> 1 to 1 + in (30..45) -> 1 to 2 + in (50..65) -> 1 to 3 + in (70..85) -> 1 to 4 + in (90..100) -> 1 to 5 + else -> 1 to skill * 5 / 100 + } + val specialSuccess: Pair = when (skill) { + 0, 5 -> 1 to 1 + 10 -> 2 to 2 + 15 -> ((criticalSuccess?.second ?: 0) + 1) to 3 + 20 -> ((criticalSuccess?.second ?: 0) + 1) to 4 + 25 -> ((criticalSuccess?.second ?: 0) + 1) to 5 + 30 -> ((criticalSuccess?.second ?: 0) + 1) to 6 + 35 -> ((criticalSuccess?.second ?: 0) + 1) to 7 + 40 -> ((criticalSuccess?.second ?: 0) + 1) to 8 + 45 -> ((criticalSuccess?.second ?: 0) + 1) to 9 + 50 -> ((criticalSuccess?.second ?: 0) + 1) to 10 + 55 -> ((criticalSuccess?.second ?: 0) + 1) to 11 + 60 -> ((criticalSuccess?.second ?: 0) + 1) to 12 + 65 -> ((criticalSuccess?.second ?: 0) + 1) to 13 + 70 -> ((criticalSuccess?.second ?: 0) + 1) to 15 + 75 -> ((criticalSuccess?.second ?: 0) + 1) to 15 + 80 -> ((criticalSuccess?.second ?: 0) + 1) to 16 + 85 -> ((criticalSuccess?.second ?: 0) + 1) to 17 + 90 -> ((criticalSuccess?.second ?: 0) + 1) to 18 + 95 -> ((criticalSuccess?.second ?: 0) + 1) to 19 + 100 -> ((criticalSuccess?.second ?: 0) + 1) to 20 + else -> ((criticalSuccess?.second ?: 0) + 1) to skill * 20 / 100 + } + val success: Pair = (specialSuccess.second + 1) to max(5, min(99, skill)) + val criticalFailure: Pair = when (skill) { + 0, 5, 10 -> 96 to 100 + 15, 20, 25, 30 -> 97 to 100 + 35, 40, 45, 50 -> 98 to 100 + 55, 60, 65, 70 -> 99 to 100 + else -> 100 to 100 + } + val failure: Pair? = if (skill >= 100) { + null + } else { + success.second + 1 to criticalFailure.first - 1 + } + + return SkillStep( + criticalSuccess = criticalSuccess, + specialSuccess = specialSuccess, + success = success, + failure = failure, + criticalFailure = criticalFailure, + ) + } +} \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/com/pixelized/desktop/lwa/composable/DecoratedBox.kt b/composeApp/src/desktopMain/kotlin/com/pixelized/desktop/lwa/composable/DecoratedBox.kt index 4c6b7cb..fa125c4 100644 --- a/composeApp/src/desktopMain/kotlin/com/pixelized/desktop/lwa/composable/DecoratedBox.kt +++ b/composeApp/src/desktopMain/kotlin/com/pixelized/desktop/lwa/composable/DecoratedBox.kt @@ -3,7 +3,6 @@ package com.pixelized.desktop.lwa.composable import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Surface @@ -20,15 +19,14 @@ import org.jetbrains.compose.ui.tooling.preview.Preview fun DecoratedBox( modifier: Modifier = Modifier, border: Color = Color(0xFFDFDFDF), - paddingValues: PaddingValues = PaddingValues(all = 8.dp), content: @Composable BoxScope.() -> Unit, ) { Box( - modifier = modifier - .border(width = 1.dp, color = border, shape = RoundedCornerShape(16.dp)) + modifier = Modifier + .border(width = 1.dp, color = border, shape = RoundedCornerShape(size = 16.dp)) .padding(all = 2.dp) .border(width = 1.dp, color = border, shape = RectangleShape) - .padding(paddingValues = paddingValues), + .then(other = modifier), content = content, ) } diff --git a/composeApp/src/desktopMain/kotlin/com/pixelized/desktop/lwa/screen/CharacterSheet.kt b/composeApp/src/desktopMain/kotlin/com/pixelized/desktop/lwa/screen/CharacterSheet.kt deleted file mode 100644 index 5fbd2db..0000000 --- a/composeApp/src/desktopMain/kotlin/com/pixelized/desktop/lwa/screen/CharacterSheet.kt +++ /dev/null @@ -1,260 +0,0 @@ -package com.pixelized.desktop.lwa.screen - -import androidx.compose.foundation.ScrollState -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import com.pixelized.desktop.lwa.composable.DecoratedBox - -@Stable -data class CharacterSheetUio( - val name: String, - val characteristics: List, - val subCharacteristics: List, - val skills: List, - val occupations: List, - val magics: List, -) { - @Stable - class Node( - val label: String, - val value: Any, - ) - - companion object { - val Koryas = CharacterSheetUio( - name = "Koryas Tissenpa", - characteristics = listOf( - Node(label = "Force", value = 10), - Node(label = "Dextérité", value = 11), - Node(label = "Constitution", value = 15), - Node(label = "Taille", value = 13), - Node(label = "Intelligence", value = 9), - Node(label = "Pouvoir", value = 15), - Node(label = "Charisme", value = 7), - ), - subCharacteristics = listOf( - Node(label = "Déplacement ", value = 10), - Node(label = "Points de vie", value = "14/14"), - Node(label = "Points de pouvoir", value = "13/13"), - Node(label = "Bonus aux dégâts", value = "1d4"), - Node(label = "Armure", value = 0), - ), - skills = listOf( - Node(label = "Bagarre", value = 75), - Node(label = "Esquive", value = 60), - Node(label = "Saisie", value = 20), - Node(label = "Lancer", value = 20), - Node(label = "Athlétisme", value = 60), - Node(label = "Acrobatie", value = 50), - Node(label = "Perception", value = 55), - Node(label = "Recherche", value = 25), - Node(label = "Empathie", value = 15), - Node(label = "Persuasion", value = 20), - Node(label = "Intimidation", value = 50), - Node(label = "Baratin", value = 20), - Node(label = "Marchandage", value = 10), - Node(label = "Escamotage", value = 20), - Node(label = "Premiers soins", value = 20), - ), - occupations = listOf( - Node(label = "Survie", value = 80), - Node(label = "Empathie (Animal)", value = 60), - Node(label = "Pistage", value = 60), - Node(label = "Discrétion", value = 60), - Node(label = "Connaissance (Herboristerie)", value = 40), - Node(label = "Artisanat (Onguent)", value = 60), - ), - magics = listOf( - Node(label = "Métamorphose (Loup)", value = 100), - ), - ) - } -} - -@OptIn(ExperimentalLayoutApi::class) -@Composable -fun CharacterSheet( - modifier: Modifier, - scrollState: ScrollState = rememberScrollState(), - width: Dp = 320.dp, - characterSheet: CharacterSheetUio = CharacterSheetUio.Koryas, -) { - Column( - modifier = Modifier - .verticalScroll(state = scrollState) - .padding(all = 16.dp) - .then(other = modifier), - verticalArrangement = Arrangement.spacedBy(space = 16.dp), - ) { - Text( - style = MaterialTheme.typography.h4, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - text = characterSheet.name, - ) - FlowRow( - maxItemsInEachRow = 3, - horizontalArrangement = Arrangement.spacedBy( - space = 16.dp, - alignment = Alignment.CenterHorizontally, - ), - verticalArrangement = Arrangement.spacedBy(space = 16.dp), - ) { - characterSheet.characteristics.forEach { - Stat( - modifier = Modifier - .width(width = (width - 32.dp) / 3) - .height(height = 120.dp), - label = it.label, - value = it.value as? Int ?: 0, - ) - } - } - DecoratedBox( - modifier = Modifier.width(width = width), - ) { - Column { - Text( - modifier = Modifier.fillMaxWidth(), - style = MaterialTheme.typography.caption, - textAlign = TextAlign.Center, - text = "Charactéristiques dérivées" - ) - characterSheet.subCharacteristics.forEach { - Characteristics( - modifier = Modifier.fillMaxWidth(), - label = it.label, - value = it.value, - ) - } - } - } - DecoratedBox( - modifier = Modifier.width(width = width), - ) { - Column { - Text( - modifier = Modifier.fillMaxWidth(), - style = MaterialTheme.typography.caption, - textAlign = TextAlign.Center, - text = "Compétences" - ) - characterSheet.skills.forEach { - Characteristics( - modifier = Modifier.fillMaxWidth(), - label = it.label, - value = it.value, - ) - } - } - } - DecoratedBox( - modifier = Modifier.width(width = width), - ) { - Column { - Text( - modifier = Modifier.fillMaxWidth(), - style = MaterialTheme.typography.caption, - textAlign = TextAlign.Center, - text = "Occupations" - ) - characterSheet.occupations.forEach { - Characteristics( - modifier = Modifier.fillMaxWidth(), - label = it.label, - value = it.value, - ) - } - } - } - DecoratedBox( - modifier = Modifier.width(width = width), - ) { - Column { - Text( - modifier = Modifier.fillMaxWidth(), - style = MaterialTheme.typography.caption, - textAlign = TextAlign.Center, - text = "Compétences magiques" - ) - characterSheet.magics.forEach { - Characteristics( - modifier = Modifier.fillMaxWidth(), - label = it.label, - value = it.value, - ) - } - } - } - } -} - -@Composable -private fun Stat( - modifier: Modifier = Modifier, - label: String, - value: Int, -) { - DecoratedBox( - modifier = modifier, - ) { - Text( - modifier = Modifier.align(alignment = Alignment.TopCenter), - style = MaterialTheme.typography.caption, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - text = label, - ) - Text( - modifier = Modifier.align(alignment = Alignment.Center), - style = MaterialTheme.typography.h4, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - text = "$value" - ) - } -} - -@Composable -private fun Characteristics( - modifier: Modifier = Modifier, - label: String, - value: Any, -) { - Row( - modifier = modifier, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - modifier = Modifier.alignByBaseline(), - style = MaterialTheme.typography.body1, - text = label - ) - Text( - modifier = Modifier.alignByBaseline(), - style = MaterialTheme.typography.body1, - fontWeight = FontWeight.Bold, - text = "$value", - ) - } -} \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/CharacterSheet.kt b/composeApp/src/desktopMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/CharacterSheet.kt new file mode 100644 index 0000000..fa67793 --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/CharacterSheet.kt @@ -0,0 +1,336 @@ +package com.pixelized.desktop.lwa.screen.characterSheet + +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Button +import androidx.compose.material.Checkbox +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.pixelized.desktop.lwa.composable.DecoratedBox +import com.pixelized.desktop.lwa.screen.characterSheet.CharacterSheetUio.Node.Type + +@Stable +data class CharacterSheetUio( + val name: String, + val characteristics: List, + val subCharacteristics: List, + val skills: List, + val occupations: List, + val magics: List, +) { + @Stable + data class Characteristic( + val label: String, + val value: String, + ) + + @Stable + data class Node( + val type: Type, + val label: String, + val value: Int, + ) { + @Stable + enum class Type { + SKILLS, + OCCUPATIONS, + MAGICS, + } + } + + companion object { + val Koryas = CharacterSheetUio( + name = "Koryas Tissenpa", + characteristics = listOf( + Characteristic(label = "Force", value = "10"), + Characteristic(label = "Dextérité", value = "11"), + Characteristic(label = "Constitution", value = "15"), + Characteristic(label = "Taille", value = "13"), + Characteristic(label = "Intelligence", value = "9"), + Characteristic(label = "Pouvoir", value = "15"), + Characteristic(label = "Charisme", value = "7"), + ), + subCharacteristics = listOf( + Characteristic(label = "Déplacement ", value = "10"), + Characteristic(label = "Points de vie", value = "14/14"), + Characteristic(label = "Points de pouvoir", value = "13/13"), + Characteristic(label = "Bonus aux dégâts", value = "1d4"), + Characteristic(label = "Armure", value = "0"), + ), + skills = listOf( + Node(type = Type.SKILLS, label = "Bagarre", value = 75), + Node(type = Type.SKILLS, label = "Esquive", value = 60), + Node(type = Type.SKILLS, label = "Saisie", value = 20), + Node(type = Type.SKILLS, label = "Lancer", value = 20), + Node(type = Type.SKILLS, label = "Athlétisme", value = 60), + Node(type = Type.SKILLS, label = "Acrobatie", value = 50), + Node(type = Type.SKILLS, label = "Perception", value = 55), + Node(type = Type.SKILLS, label = "Recherche", value = 25), + Node(type = Type.SKILLS, label = "Empathie", value = 15), + Node(type = Type.SKILLS, label = "Persuasion", value = 20), + Node(type = Type.SKILLS, label = "Intimidation", value = 50), + Node(type = Type.SKILLS, label = "Baratin", value = 20), + Node(type = Type.SKILLS, label = "Marchandage", value = 10), + Node(type = Type.SKILLS, label = "Escamotage", value = 20), + Node(type = Type.SKILLS, label = "Premiers soins", value = 20), + ), + occupations = listOf( + Node(type = Type.OCCUPATIONS, label = "Survie", value = 80), + Node(type = Type.OCCUPATIONS, label = "Empathie (Animal)", value = 60), + Node(type = Type.OCCUPATIONS, label = "Pistage", value = 60), + Node(type = Type.OCCUPATIONS, label = "Discrétion", value = 60), + Node(type = Type.OCCUPATIONS, label = "Connaissance (Herboristerie)", value = 40), + Node(type = Type.OCCUPATIONS, label = "Artisanat (Onguent)", value = 60), + ), + magics = listOf( + Node(type = Type.MAGICS, label = "Métamorphose (Loup)", value = 100), + ), + ) + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun CharacterSheet( + modifier: Modifier, + scrollState: ScrollState = rememberScrollState(), + width: Dp = 320.dp, + characterSheet: CharacterSheetUio = CharacterSheetUio.Koryas, + onCharacteristic: (characteristic: CharacterSheetUio.Characteristic) -> Unit, + onSkill: (skill: CharacterSheetUio.Node) -> Unit, +) { + Column( + modifier = Modifier + .verticalScroll(state = scrollState) + .padding(all = 16.dp) + .then(other = modifier), + verticalArrangement = Arrangement.spacedBy(space = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + modifier = Modifier.padding(bottom = 16.dp), + style = MaterialTheme.typography.h4, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + text = characterSheet.name, + ) + FlowRow( + maxItemsInEachRow = 3, + horizontalArrangement = Arrangement.spacedBy( + space = 16.dp, + alignment = Alignment.CenterHorizontally, + ), + verticalArrangement = Arrangement.spacedBy(space = 16.dp), + ) { + characterSheet.characteristics.forEach { + Stat( + modifier = Modifier + .width(width = width / 3 - 32.dp) + .height(height = 112.dp), + characteristic = it, + onClick = { onCharacteristic(it) }, + ) + } + } + DecoratedBox( + modifier = Modifier + .width(width = width) + .padding(vertical = 8.dp), + ) { + Column { + Text( + modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp), + style = MaterialTheme.typography.caption, + textAlign = TextAlign.Center, + text = "Charactéristiques dérivées" + ) + characterSheet.subCharacteristics.forEach { + Characteristics( + modifier = Modifier.fillMaxWidth(), + characteristic = it, + ) + } + } + } + DecoratedBox( + modifier = Modifier + .width(width = width) + .padding(vertical = 8.dp), + ) { + Column { + Text( + modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp), + style = MaterialTheme.typography.caption, + textAlign = TextAlign.Center, + text = "Compétences", + ) + characterSheet.skills.forEach { + Skill( + modifier = Modifier.fillMaxWidth(), + label = it.label, + value = it.value, + onClick = { onSkill(it) }, + ) + } + } + } + DecoratedBox( + modifier = Modifier + .width(width = width) + .padding(vertical = 8.dp), + ) { + Column { + Text( + modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp), + style = MaterialTheme.typography.caption, + textAlign = TextAlign.Center, + text = "Occupations" + ) + characterSheet.occupations.forEach { + Skill( + modifier = Modifier.fillMaxWidth(), + label = it.label, + value = it.value, + onClick = { onSkill(it) }, + ) + } + } + } + DecoratedBox( + modifier = Modifier + .width(width = width) + .padding(vertical = 8.dp), + ) { + Column { + Text( + modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp), + style = MaterialTheme.typography.caption, + textAlign = TextAlign.Center, + text = "Compétences magiques" + ) + characterSheet.magics.forEach { + Skill( + modifier = Modifier.fillMaxWidth(), + label = it.label, + value = it.value, + onClick = { onSkill(it) }, + ) + } + } + } + } +} + +@Composable +private fun Stat( + modifier: Modifier = Modifier, + paddingValues: PaddingValues = PaddingValues(all = 8.dp), + characteristic: CharacterSheetUio.Characteristic, + onClick: () -> Unit, +) { + DecoratedBox( + modifier = Modifier + .clickable(onClick = onClick) + .padding(paddingValues = paddingValues) + .then(other = modifier), + ) { + Text( + modifier = Modifier.align(alignment = Alignment.TopCenter), + style = MaterialTheme.typography.caption, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + text = characteristic.label, + ) + Text( + modifier = Modifier.align(alignment = Alignment.Center), + style = MaterialTheme.typography.h4, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + text = characteristic.value + ) + } +} + +@Composable +private fun Characteristics( + modifier: Modifier = Modifier, + paddingValues: PaddingValues = PaddingValues(horizontal = 8.dp), + characteristic: CharacterSheetUio.Characteristic, +) { + Row( + modifier = Modifier + .padding(paddingValues = paddingValues) + .then(other = modifier), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + modifier = Modifier.alignByBaseline(), + style = MaterialTheme.typography.body1, + text = characteristic.label + ) + Text( + modifier = Modifier.alignByBaseline(), + style = MaterialTheme.typography.body1, + fontWeight = FontWeight.Bold, + text = characteristic.value, + ) + } +} + +@Composable +private fun Skill( + modifier: Modifier = Modifier, + paddingValues: PaddingValues = PaddingValues(horizontal = 8.dp), + label: String, + value: Any, + onClick: () -> Unit, +) { + Row( + modifier = Modifier + .clickable(onClick = onClick) + .padding(paddingValues = paddingValues) + .then(other = modifier), + horizontalArrangement = Arrangement.spacedBy(space = 4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + modifier = Modifier.weight(1f), + style = MaterialTheme.typography.body1, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + text = label + ) + Text( + style = MaterialTheme.typography.body1, + fontWeight = FontWeight.Bold, + text = "$value", + ) + Checkbox( + modifier = Modifier.size(size = 32.dp), + checked = false, + onCheckedChange = { } + ) + } +} \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/CharacterSheetViewModel.kt b/composeApp/src/desktopMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/CharacterSheetViewModel.kt new file mode 100644 index 0000000..56f5f5d --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/com/pixelized/desktop/lwa/screen/characterSheet/CharacterSheetViewModel.kt @@ -0,0 +1,19 @@ +package com.pixelized.desktop.lwa.screen.characterSheet + +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.ViewModel + +class CharacterSheetViewModel : ViewModel() { + + private val _sheet = mutableStateOf(null) + val sheet: State get() = _sheet + + fun showCharacterSheet() { + _sheet.value = CharacterSheetUio.Koryas + } + + fun hideCharacterSheet() { + _sheet.value = null + } +} \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/com/pixelized/desktop/lwa/screen/overlay/BlurOverlay.kt b/composeApp/src/desktopMain/kotlin/com/pixelized/desktop/lwa/screen/overlay/BlurOverlay.kt new file mode 100644 index 0000000..6c5c0a1 --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/com/pixelized/desktop/lwa/screen/overlay/BlurOverlay.kt @@ -0,0 +1,51 @@ +package com.pixelized.desktop.lwa.screen.overlay + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.blur +import androidx.lifecycle.viewmodel.compose.viewModel + +@Composable +fun BlurOverlay( + viewModel: BlurOverlayViewModel = viewModel { BlurOverlayViewModel() }, + overlay: @Composable BoxScope.() -> Unit, + content: @Composable BoxScope.() -> Unit, +) { + Box { + Box( + modifier = Modifier + .fillMaxSize() + .blur(radius = viewModel.blur.value), + content = content, + ) + + AnimatedContent( + targetState = viewModel.overlay.value, + transitionSpec = { + val enter = fadeIn() + slideInVertically { 64 } + val exit = fadeOut() + slideOutVertically { 64 } + enter togetherWith exit + }, + ) { roll -> + when (roll) { + true -> Box( + modifier = Modifier.fillMaxSize(), + content = overlay, + ) + + else -> Box( + modifier = Modifier.fillMaxSize() + ) + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/com/pixelized/desktop/lwa/screen/overlay/BlurOverlayViewModel.kt b/composeApp/src/desktopMain/kotlin/com/pixelized/desktop/lwa/screen/overlay/BlurOverlayViewModel.kt new file mode 100644 index 0000000..bc7b4dc --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/com/pixelized/desktop/lwa/screen/overlay/BlurOverlayViewModel.kt @@ -0,0 +1,33 @@ +package com.pixelized.desktop.lwa.screen.overlay + +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.lifecycle.ViewModel + +class BlurOverlayViewModel : ViewModel() { + private val _overlay = mutableStateOf(false) + + val overlay: State + get() = _overlay + + val blur: State + @Composable + get() = animateDpAsState( + targetValue = when (overlay.value) { + true -> 16.dp + else -> 0.dp + } + ) + + fun show() { + _overlay.value = true + } + + fun hide() { + _overlay.value = false + } +} \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/com/pixelized/desktop/lwa/screen/roll/RollPage.kt b/composeApp/src/desktopMain/kotlin/com/pixelized/desktop/lwa/screen/roll/RollPage.kt new file mode 100644 index 0000000..8180ca7 --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/com/pixelized/desktop/lwa/screen/roll/RollPage.kt @@ -0,0 +1,150 @@ +package com.pixelized.desktop.lwa.screen.roll + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Shadow +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import kotlinx.coroutines.launch +import lwacharactersheet.composeapp.generated.resources.Res +import lwacharactersheet.composeapp.generated.resources.ic_d20_32dp +import org.jetbrains.compose.resources.painterResource + +@Stable +data class RollUio( + val label: String, + val value: Int, +) + +@Stable +data class RollResultUio( + val label: String, + val value: Int, +) + +@Composable +fun RollPage( + viewModel: RollViewModel = viewModel { RollViewModel() }, + onDismissRequest: () -> Unit, +) { + val scope = rememberCoroutineScope() + + Column( + modifier = Modifier.fillMaxSize() + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = onDismissRequest, + ) + .padding(all = 32.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + style = MaterialTheme.typography.caption, + textAlign = TextAlign.Center, + overflow = TextOverflow.Ellipsis, + text = "Jet de :", + ) + Text( + style = MaterialTheme.typography.h5, + textAlign = TextAlign.Center, + overflow = TextOverflow.Ellipsis, + text = viewModel.roll.value.label, + ) + Text( + style = MaterialTheme.typography.caption, + textAlign = TextAlign.Center, + overflow = TextOverflow.Ellipsis, + text = "Réussite en dessous de : ${viewModel.roll.value.value}", + ) + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy( + space = 24.dp, + alignment = Alignment.CenterVertically, + ), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Box( + contentAlignment = Alignment.Center + ) { + Icon( + modifier = Modifier + .clip(shape = CircleShape) + .clickable { scope.launch { viewModel.roll() } } + .padding(all = 24.dp) + .size(size = 128.dp) + .graphicsLayer { + this.alpha = 0.8f + this.rotationZ = viewModel.rollRotation.value + }, + tint = MaterialTheme.colors.onSurface, + painter = painterResource(Res.drawable.ic_d20_32dp), + contentDescription = null, + ) + AnimatedContent( + targetState = viewModel.result.value?.value?.toString() ?: "", + transitionSpec = { + val enter = fadeIn() + slideInVertically { 32 } + val exit = fadeOut() + slideOutVertically { -32 } + enter togetherWith exit + } + ) { label -> + Text( + modifier = Modifier.width(width = 128.dp), + style = MaterialTheme.typography.h3.copy( + shadow = Shadow( + color = MaterialTheme.colors.surface, + offset = Offset.Zero, + blurRadius = 8f, + ) + ), + textAlign = TextAlign.Center, + color = MaterialTheme.colors.onSurface, + text = label, + ) + } + } + AnimatedContent( + targetState = viewModel.result.value?.label ?: "", + transitionSpec = { fadeIn() togetherWith fadeOut() }, + ) { value -> + Text( + modifier = Modifier.fillMaxWidth(), + style = MaterialTheme.typography.h4, + textAlign = TextAlign.Center, + text = value, + ) + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/com/pixelized/desktop/lwa/screen/roll/RollViewModel.kt b/composeApp/src/desktopMain/kotlin/com/pixelized/desktop/lwa/screen/roll/RollViewModel.kt new file mode 100644 index 0000000..b8e2eee --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/com/pixelized/desktop/lwa/screen/roll/RollViewModel.kt @@ -0,0 +1,98 @@ +package com.pixelized.desktop.lwa.screen.roll + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.ViewModel +import com.pixelized.desktop.lwa.business.RollUseCase +import com.pixelized.desktop.lwa.business.SkillStepUseCase +import com.pixelized.desktop.lwa.screen.characterSheet.CharacterSheetUio +import kotlinx.coroutines.Job +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking + +class RollViewModel : ViewModel() { + private val _roll = mutableStateOf(RollUio(label = "", value = 0)) + val roll: State get() = _roll + + private var rollJob: Job? = null + private lateinit var rollStep: SkillStepUseCase.SkillStep + val rollRotation = Animatable(0f) + + private val _result = mutableStateOf(null) + val result: State get() = _result + + fun prepareRoll(node: CharacterSheetUio.Node) { + val step = SkillStepUseCase.computeSkillStep( + skill = node.value, + ) + prepareRoll( + label = node.label, + step = step, + ) + } + + fun prepareRoll(characteristic: CharacterSheetUio.Characteristic) { + val step = SkillStepUseCase.computeSkillStep( + skill = (characteristic.value.toIntOrNull() ?: 0) * 5 + ) + prepareRoll( + label = characteristic.label, + step = step, + ) + } + + private fun prepareRoll( + label: String, + step: SkillStepUseCase.SkillStep, + ) { + runBlocking { rollRotation.snapTo(0f) } + rollStep = step + _result.value = null + _roll.value = RollUio( + label = label, + value = step.successRange.last + ) + } + + suspend fun roll() { + coroutineScope { + _result.value = null + + rollJob?.cancel() + rollJob = launch { + launch { + rollRotation.animateTo( + targetValue = rollRotation.value.let { it - it % 360 } + 360f * 3, + animationSpec = spring( + dampingRatio = Spring.DampingRatioNoBouncy, + stiffness = Spring.StiffnessLow, + ) + ) + } + launch { + delay(500) + + val d100 = RollUseCase.rollD100() + + _result.value = RollResultUio( + label = when (d100) { + // TODO wording + in rollStep.criticalSuccessRange -> "Réussite critique" + in rollStep.specialSuccessRange -> "Réussite spéciale" + in rollStep.successRange -> "Réussite" + in rollStep.failureRange -> "Échec" + in rollStep.criticalFailureRange -> "Échec critique" + else -> "" + }, + value = d100, + ) + } + } + } + } +} \ No newline at end of file