Add a basic character sheet.

This commit is contained in:
Thomas Andres Gomez 2024-11-02 23:18:01 +01:00
parent 9ddd6018fd
commit 6e4f91e007
13 changed files with 871 additions and 329 deletions

View file

@ -1,36 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="600dp"
android:height="600dp"
android:viewportWidth="600"
android:viewportHeight="600">
<path
android:pathData="M301.21,418.53C300.97,418.54 300.73,418.56 300.49,418.56C297.09,418.59 293.74,417.72 290.79,416.05L222.6,377.54C220.63,376.43 219,374.82 217.85,372.88C216.7,370.94 216.09,368.73 216.07,366.47L216.07,288.16C216.06,287.32 216.09,286.49 216.17,285.67C216.38,283.54 216.91,281.5 217.71,279.6L199.29,268.27L177.74,256.19C175.72,260.43 174.73,265.23 174.78,270.22L174.79,387.05C174.85,393.89 178.57,400.2 184.53,403.56L286.26,461.02C290.67,463.51 295.66,464.8 300.73,464.76C300.91,464.76 301.09,464.74 301.27,464.74C301.24,449.84 301.22,439.23 301.22,439.23L301.21,418.53Z"
android:fillColor="#041619"
android:fillType="nonZero"/>
<path
android:pathData="M409.45,242.91L312.64,188.23C303.64,183.15 292.58,183.26 283.68,188.51L187.92,245C183.31,247.73 179.93,251.62 177.75,256.17L177.74,256.19L199.29,268.27L217.71,279.6C217.83,279.32 217.92,279.02 218.05,278.74C218.24,278.36 218.43,277.98 218.64,277.62C219.06,276.88 219.52,276.18 220.04,275.51C221.37,273.8 223.01,272.35 224.87,271.25L289.06,233.39C290.42,232.59 291.87,231.96 293.39,231.51C295.53,230.87 297.77,230.6 300,230.72C302.98,230.88 305.88,231.73 308.47,233.2L373.37,269.85C375.54,271.08 377.49,272.68 379.13,274.57C379.68,275.19 380.18,275.85 380.65,276.53C380.86,276.84 381.05,277.15 381.24,277.47L397.79,266.39L420.34,252.93L420.31,252.88C417.55,248.8 413.77,245.35 409.45,242.91Z"
android:fillColor="#37BF6E"
android:fillType="nonZero"/>
<path
android:pathData="M381.24,277.47C381.51,277.92 381.77,278.38 382.01,278.84C382.21,279.24 382.39,279.65 382.57,280.06C382.91,280.88 383.19,281.73 383.41,282.59C383.74,283.88 383.92,285.21 383.93,286.57L383.93,361.1C383.96,363.95 383.35,366.77 382.16,369.36C381.93,369.86 381.69,370.35 381.42,370.83C379.75,373.79 377.32,376.27 374.39,378L310.2,415.87C307.47,417.48 304.38,418.39 301.21,418.53L301.22,439.23C301.22,439.23 301.24,449.84 301.27,464.74C306.1,464.61 310.91,463.3 315.21,460.75L410.98,404.25C419.88,399 425.31,389.37 425.22,379.03L425.22,267.85C425.17,262.48 423.34,257.34 420.34,252.93L397.79,266.39L381.24,277.47Z"
android:fillColor="#3870B2"
android:fillType="nonZero"/>
<path
android:pathData="M177.75,256.17C179.93,251.62 183.31,247.73 187.92,245L283.68,188.51C292.58,183.26 303.64,183.15 312.64,188.23L409.45,242.91C413.77,245.35 417.55,248.8 420.31,252.88L420.34,252.93L498.59,206.19C494.03,199.46 487.79,193.78 480.67,189.75L320.86,99.49C306.01,91.1 287.75,91.27 273.07,99.95L114.99,193.2C107.39,197.69 101.81,204.11 98.21,211.63L177.74,256.19L177.75,256.17ZM301.27,464.74C301.09,464.74 300.91,464.76 300.73,464.76C295.66,464.8 290.67,463.51 286.26,461.02L184.53,403.56C178.57,400.2 174.85,393.89 174.79,387.05L174.78,270.22C174.73,265.23 175.72,260.43 177.74,256.19L98.21,211.63C94.86,218.63 93.23,226.58 93.31,234.82L93.31,427.67C93.42,438.97 99.54,449.37 109.4,454.92L277.31,549.77C284.6,553.88 292.84,556.01 301.2,555.94L301.2,555.8C301.39,543.78 301.33,495.26 301.27,464.74Z"
android:strokeWidth="10"
android:fillColor="#00000000"
android:strokeColor="#083042"
android:fillType="nonZero"/>
<path
android:pathData="M498.59,206.19L420.34,252.93C423.34,257.34 425.17,262.48 425.22,267.85L425.22,379.03C425.31,389.37 419.88,399 410.98,404.25L315.21,460.75C310.91,463.3 306.1,464.61 301.27,464.74C301.33,495.26 301.39,543.78 301.2,555.8L301.2,555.94C309.48,555.87 317.74,553.68 325.11,549.32L483.18,456.06C497.87,447.39 506.85,431.49 506.69,414.43L506.69,230.91C506.6,222.02 503.57,213.5 498.59,206.19Z"
android:strokeWidth="10"
android:fillColor="#00000000"
android:strokeColor="#083042"
android:fillType="nonZero"/>
<path
android:pathData="M301.2,555.94C292.84,556.01 284.6,553.88 277.31,549.76L109.4,454.92C99.54,449.37 93.42,438.97 93.31,427.67L93.31,234.82C93.23,226.58 94.86,218.63 98.21,211.63C101.81,204.11 107.39,197.69 114.99,193.2L273.07,99.95C287.75,91.27 306.01,91.1 320.86,99.49L480.67,189.75C487.79,193.78 494.03,199.46 498.59,206.19C503.57,213.5 506.6,222.02 506.69,230.91L506.69,414.43C506.85,431.49 497.87,447.39 483.18,456.06L325.11,549.32C317.74,553.68 309.48,555.87 301.2,555.94Z"
android:strokeWidth="10"
android:fillColor="#00000000"
android:strokeColor="#083042"
android:fillType="nonZero"/>
</vector>

View file

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#FF000000"
android:pathData="M245.5,430.1L34.4,741.9c-6.2,10 0.2,23 11.9,24.3l416.5,44.1 -217.3,-380.2zM46.8,630.9L197.4,386.2 44.1,294.2c-5.3,-3.2 -12.1,0.6 -12.1,6.9v325.6c0,8.1 10.6,11.1 14.8,4.2zM68.5,847.2l388.8,175.3c10.6,4.9 22.7,-2.9 22.7,-14.5v-131.3l-407.1,-44.6c-8.9,-1 -12.5,11.2 -4.4,15.1zM230.9,331.6L390.8,45.8c8.7,-14.1 -7.2,-30.5 -21.6,-22.3L67.6,220.7c-4.9,3.2 -4.8,10.5 0.3,13.6l163.1,97.4zM512,352h218.4L539.2,15.2C533,5.1 522.5,0 512,0s-21,5.1 -27.3,15.2L293.6,352L512,352zM979.9,294.2l-153.3,92 150.6,244.7c4.2,6.9 14.8,3.9 14.8,-4.2L992,301.1c0,-6.2 -6.8,-10.1 -12.1,-6.9zM793,331.6l163.1,-97.4c5.1,-3 5.2,-10.3 0.3,-13.6l-301.6,-197.2c-14.4,-8.2 -30.2,8.2 -21.6,22.3l159.9,285.9zM951.1,832L544,876.6v131.3c0,11.7 12.1,19.4 22.7,14.5l388.8,-175.3c8.1,-3.9 4.5,-16.1 -4.4,-15.1zM778.5,430.1l-217.3,380.2 416.5,-44.1c11.7,-1.3 18,-14.3 11.9,-24.3L778.5,430.1zM512,416L311.1,416L512,767.5 712.8,416L512,416z" />
</vector>

View file

@ -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<CharacterSheetUio?>(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()
}
)
},
)
}
}
}
}
}

View file

@ -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()
}
}

View file

@ -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<Int, Int>?,
specialSuccess: Pair<Int, Int>,
success: Pair<Int, Int>,
failure: Pair<Int, Int>?,
criticalFailure: Pair<Int, Int>,
) : 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<Int, Int>? = 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<Int, Int> = 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<Int, Int> = (specialSuccess.second + 1) to max(5, min(99, skill))
val criticalFailure: Pair<Int, Int> = 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<Int, Int>? = if (skill >= 100) {
null
} else {
success.second + 1 to criticalFailure.first - 1
}
return SkillStep(
criticalSuccess = criticalSuccess,
specialSuccess = specialSuccess,
success = success,
failure = failure,
criticalFailure = criticalFailure,
)
}
}

View file

@ -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,
)
}

View file

@ -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<Node>,
val subCharacteristics: List<Node>,
val skills: List<Node>,
val occupations: List<Node>,
val magics: List<Node>,
) {
@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",
)
}
}

View file

@ -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<Characteristic>,
val subCharacteristics: List<Characteristic>,
val skills: List<Node>,
val occupations: List<Node>,
val magics: List<Node>,
) {
@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 = { }
)
}
}

View file

@ -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<CharacterSheetUio?>(null)
val sheet: State<CharacterSheetUio?> get() = _sheet
fun showCharacterSheet() {
_sheet.value = CharacterSheetUio.Koryas
}
fun hideCharacterSheet() {
_sheet.value = null
}
}

View file

@ -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()
)
}
}
}
}

View file

@ -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<Boolean>
get() = _overlay
val blur: State<Dp>
@Composable
get() = animateDpAsState(
targetValue = when (overlay.value) {
true -> 16.dp
else -> 0.dp
}
)
fun show() {
_overlay.value = true
}
fun hide() {
_overlay.value = false
}
}

View file

@ -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,
)
}
}
}
}

View file

@ -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<RollUio> get() = _roll
private var rollJob: Job? = null
private lateinit var rollStep: SkillStepUseCase.SkillStep
val rollRotation = Animatable(0f)
private val _result = mutableStateOf<RollResultUio?>(null)
val result: State<RollResultUio?> 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,
)
}
}
}
}
}