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