Update the app with some features skeleton.

Change the navigation system
Add a basic file system management to save the charactersheet.
Add a common source code directory & commonTest module.
Add test to the business class.
This commit is contained in:
Thomas Andres Gomez 2024-11-04 23:33:11 +01:00
parent d74a5fcd7c
commit 65aa53890f
34 changed files with 1412 additions and 541 deletions

View file

@ -21,9 +21,14 @@ kotlin {
implementation(compose.components.uiToolingPreview)
implementation(libs.androidx.lifecycle.viewmodel)
implementation(libs.androidx.lifecycle.runtime.compose)
implementation("org.jetbrains.androidx.navigation:navigation-compose:2.8.0-alpha10")
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.datastore.preferences)
}
commonTest.dependencies {
implementation(libs.kotlin.test)
}
desktopMain.dependencies {
implementation(compose.desktop.currentOs)
implementation(libs.kotlinx.coroutines.swing)
@ -40,6 +45,9 @@ compose.desktop {
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
packageName = "com.pixelized.desktop.lwa"
packageVersion = "1.0.0"
// Use system theming fot the app toolbars.
jvmArgs("-Dapple.awt.application.appearance=system")
}
}
}

Binary file not shown.

View file

@ -5,10 +5,8 @@ object RollUseCase {
/**
* (Math.random() * 100 + 1).toInt()
* TODO : test.
*/
fun rollD100(): Int {
return d100.random()
}
}

View file

@ -0,0 +1,10 @@
package com.pixelized.desktop.lwa.business
import kotlin.math.truncate
object SkillNormalizerUseCase {
fun normalize(value: Int): Int {
return (truncate(value.toFloat() / 5f) * 5f).toInt()
}
}

View file

@ -61,7 +61,7 @@ object SkillStepUseCase {
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
70 -> ((criticalSuccess?.second ?: 0) + 1) to 14
75 -> ((criticalSuccess?.second ?: 0) + 1) to 15
80 -> ((criticalSuccess?.second ?: 0) + 1) to 16
85 -> ((criticalSuccess?.second ?: 0) + 1) to 17

View file

@ -0,0 +1,10 @@
package com.pixelized.desktop.lwa
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.core.Preferences
import okio.Path.Companion.toPath
fun createDataStore(producePath: () -> String): DataStore<Preferences> {
return PreferenceDataStoreFactory.createWithPath(produceFile = { producePath().toPath() })
}

View file

@ -0,0 +1,44 @@
package com.pixelized.desktop.lwa.repository.characterSheet
import java.io.Serializable
data class CharacterSheet(
val id: String,
val name: String,
// characteristics
val strength: Int,
val dexterity: Int,
val constitution: Int,
val height: Int,
val intelligence: Int,
val power: Int,
val charisma: Int,
// sub characteristics
val movement: Int,
val currentHp: Int,
val maxHp: Int,
val currentPP: Int,
val maxPP: Int,
val damageBonus: String,
val armor: Int,
// skills
val skills: List<Skill>,
// occupations
val occupations: List<Skill>,
// magic skill
val magics: List<Skill>,
// attack
val attacks: List<Roll>,
) : Serializable {
data class Skill(
val label: String,
val value: Int,
val used: Boolean,
) : Serializable
data class Roll(
val label: String,
val roll: String,
) : Serializable
}

View file

@ -0,0 +1,35 @@
package com.pixelized.desktop.lwa.repository.characterSheet
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.byteArrayPreferencesKey
import androidx.datastore.preferences.core.edit
import com.pixelized.desktop.lwa.utils.extention.fromByteArray
import com.pixelized.desktop.lwa.utils.extention.toByteArray
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
class CharacterSheetPreference(
private val dataStore: DataStore<Preferences>,
) {
suspend fun save(sheets: List<CharacterSheet>) {
dataStore.edit {
it[characterSheetKey] = sheets.toByteArray()
}
}
suspend fun load(): List<CharacterSheet> {
return loadFlow().first()
}
fun loadFlow(): Flow<List<CharacterSheet>> {
return dataStore.data.map {
it[characterSheetKey]?.fromByteArray<List<CharacterSheet>>() ?: emptyList()
}
}
companion object {
private val characterSheetKey = byteArrayPreferencesKey("CharacterSheetsPrefKey")
}
}

View file

@ -0,0 +1,52 @@
package com.pixelized.desktop.lwa.repository.characterSheet
import com.pixelized.desktop.lwa.createDataStore
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
object CharacterSheetRepository {
private val scope = CoroutineScope(Dispatchers.IO)
private val preferences = CharacterSheetPreference(
dataStore = createDataStore { "characterssheet.preferences_pb" }
)
fun characterSheet(): StateFlow<List<CharacterSheet>> {
return preferences.loadFlow()
.stateIn(
scope = scope,
started = SharingStarted.Eagerly,
initialValue = emptyList()
)
}
fun characterSheet(id: String): StateFlow<CharacterSheet?> {
return preferences.loadFlow()
.map { sheets ->
sheets.firstOrNull { sheet -> sheet.id == id }
}
.stateIn(
scope = scope,
started = SharingStarted.Eagerly,
initialValue = null
)
}
suspend fun save(characterSheet: CharacterSheet) {
val savedSheets = preferences.load().toMutableList()
val savedIndex = savedSheets.indexOfFirst { it.id == characterSheet.id }
if (savedIndex >= 0) {
// this sheet is already saved. update it
savedSheets[savedIndex] = characterSheet
} else {
// add the character sheet to the list.
savedSheets.add(characterSheet)
}
// save the list of characters sheet.
preferences.save(sheets = savedSheets)
}
}

View file

@ -0,0 +1,27 @@
package com.pixelized.desktop.lwa.utils.extention
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
@Suppress("UNCHECKED_CAST")
fun <T> ByteArray.fromByteArray(): T {
val byteArrayInputStream = ByteArrayInputStream(this)
val objectInput = ObjectInputStream(byteArrayInputStream)
val result = objectInput.readObject() as T
objectInput.close()
byteArrayInputStream.close()
return result
}
fun Any.toByteArray(): ByteArray {
val byteArrayOutputStream = ByteArrayOutputStream()
val objectOutputStream = ObjectOutputStream(byteArrayOutputStream)
objectOutputStream.writeObject(this)
objectOutputStream.flush()
val result = byteArrayOutputStream.toByteArray()
byteArrayOutputStream.close()
objectOutputStream.close()
return result
}

View file

@ -0,0 +1,34 @@
package com.pixelized.desktop.lwa.utils.extention
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.produceState
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.withContext
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
@Suppress("StateFlowValueCalledInComposition")
@Composable
fun <T, R> StateFlow<T>.collectAsState(
context: CoroutineContext = EmptyCoroutineContext,
convert: (T) -> R,
): State<R> = collectAsState(
initial = value,
context = context,
convert = convert,
)
@Composable
fun <T, R> Flow<T>.collectAsState(
initial: T,
context: CoroutineContext = EmptyCoroutineContext,
convert: (T) -> R,
): State<R> = produceState(convert(initial), this, context) {
if (context == EmptyCoroutineContext) {
collect { value = convert(it) }
} else withContext(context) {
collect { value = convert(it) }
}
}

View file

@ -0,0 +1,44 @@
package com.pixelized.desktop.lwa.business
import org.junit.Test
class RollUseCaseTest {
companion object {
private const val ROLL_COUNT = 1000000000
}
@Test
fun testRoll1D100() {
val result = build1D100ResultSet()
println("Testing if with $ROLL_COUNT rolls we have at least 1 result of each value.")
assert(result.all { it > 0 }) {
"Maybe a false negative, we expected that all values should have a result.\nroll amount: $ROLL_COUNT - result:$result"
}
}
@Test
fun testRoll1D100_() {
val result = build1D100ResultSet()
val delta = 0.001f
val median = (ROLL_COUNT / 100).let {
(it * (1f - delta)).toInt()..(it * (1f + delta)).toInt()
}
println("Testing if with $ROLL_COUNT rolls we have at least all results in $median.")
assert(result.all { it in median }) {
"Maybe a false negative, we expected that all values should be in $median a result.\nroll amount: $ROLL_COUNT - result:$result"
}
}
private fun build1D100ResultSet(count: Int = ROLL_COUNT): List<Int> {
val result = MutableList(100) { 0 }
repeat(count) {
val roll = RollUseCase.rollD100()
result[roll - 1] += 1
}
return result
}
}

View file

@ -0,0 +1,29 @@
package com.pixelized.desktop.lwa.business
import com.pixelized.desktop.lwa.business.SkillNormalizerUseCase.normalize
import org.junit.Test
class SkillNormalizerUseCaseText {
@Test
fun testNormalization() {
val samples = listOf(
0 to 0,
1 to 0,
2 to 0,
3 to 0,
4 to 0,
5 to 5,
6 to 5,
7 to 5,
8 to 5,
9 to 5,
10 to 10,
)
samples.forEach { (value, expected) ->
assert(normalize(value) == expected) {
"$value should be normalized to $expected"
}
}
}
}

View file

@ -0,0 +1,180 @@
package com.pixelized.desktop.lwa.business
import org.junit.Test
class SkillStepUseCaseTest {
companion object {
val EXPECTED = hashMapOf(
0 to listOf(-1..-1, 1..1, 2..5, 6..95, 96..100),
5 to listOf(-1..-1, 1..1, 2..5, 6..95, 96..100),
10 to listOf(1..1, 2..2, 3..10, 11..95, 96..100),
15 to listOf(1..1, 2..3, 4..15, 16..96, 97..100),
20 to listOf(1..1, 2..4, 5..20, 21..96, 97..100),
25 to listOf(1..1, 2..5, 6..25, 26..96, 97..100),
30 to listOf(1..2, 3..6, 7..30, 31..96, 97..100),
35 to listOf(1..2, 3..7, 8..35, 36..97, 98..100),
40 to listOf(1..2, 3..8, 9..40, 41..97, 98..100),
45 to listOf(1..2, 3..9, 10..45, 46..97, 98..100),
50 to listOf(1..3, 4..10, 11..50, 51..97, 98..100),
55 to listOf(1..3, 4..11, 12..55, 56..98, 99..100),
60 to listOf(1..3, 4..12, 13..60, 61..98, 99..100),
65 to listOf(1..3, 4..13, 14..65, 66..98, 99..100),
70 to listOf(1..4, 5..14, 15..70, 71..98, 99..100),
75 to listOf(1..4, 5..15, 16..75, 76..99, 100..100),
80 to listOf(1..4, 5..16, 17..80, 81..99, 100..100),
85 to listOf(1..4, 5..17, 18..85, 86..99, 100..100),
90 to listOf(1..5, 6..18, 19..90, 91..99, 100..100),
95 to listOf(1..5, 6..19, 20..95, 96..99, 100..100),
100 to listOf(1..5, 6..20, 21..99, -1..-1, 100..100),
)
}
@Test
fun testStepForSkillAt_0() {
testStepForSkillStep(skill = 0)
}
@Test
fun testStepForSkillAt_5() {
testStepForSkillStep(skill = 5)
}
@Test
fun testStepForSkillAt10() {
testStepForSkillStep(skill = 10)
}
@Test
fun testStepForSkillAt15() {
testStepForSkillStep(skill = 15)
}
@Test
fun testStepForSkillAt20() {
testStepForSkillStep(skill = 20)
}
@Test
fun testStepForSkillAt25() {
testStepForSkillStep(skill = 25)
}
@Test
fun testStepForSkillAt30() {
testStepForSkillStep(skill = 30)
}
@Test
fun testStepForSkillAt35() {
testStepForSkillStep(skill = 35)
}
@Test
fun testStepForSkillAt40() {
testStepForSkillStep(skill = 40)
}
@Test
fun testStepForSkillAt45() {
testStepForSkillStep(skill = 45)
}
@Test
fun testStepForSkillAt50() {
testStepForSkillStep(skill = 50)
}
@Test
fun testStepForSkillAt55() {
testStepForSkillStep(skill = 55)
}
@Test
fun testStepForSkillAt60() {
testStepForSkillStep(skill = 60)
}
@Test
fun testStepForSkillAt65() {
testStepForSkillStep(skill = 65)
}
@Test
fun testStepForSkillAt70() {
testStepForSkillStep(skill = 70)
}
@Test
fun testStepForSkillAt75() {
testStepForSkillStep(skill = 75)
}
@Test
fun testStepForSkillAt80() {
testStepForSkillStep(skill = 80)
}
@Test
fun testStepForSkillAt85() {
testStepForSkillStep(skill = 85)
}
@Test
fun testStepForSkillAt90() {
testStepForSkillStep(skill = 90)
}
@Test
fun testStepForSkillAt95() {
testStepForSkillStep(skill = 95)
}
@Test
fun testStepForSkillAt100() {
testStepForSkillStep(skill = 100)
}
@Test
fun testStepForSkillAbove100() {
val levels = List(50) { 100 + it * 5 }
levels.forEach { skillLevel ->
testStepForSkillStep(
skill = skillLevel,
expectedCriticalSuccessRange = 1..(skillLevel * 5 / 100),
expectedSpecialSuccessRange = (skillLevel * 5 / 100 + 1)..(skillLevel * 20 / 100),
expectedSuccessRange = (skillLevel * 20 / 100 + 1)..99,
expectedFailureRange = -1..-1,
expectedCriticalFailureRange = 100..100,
)
}
}
private fun testStepForSkillStep(
skill: Int,
expectedCriticalSuccessRange: IntRange = EXPECTED[skill]!![0],
expectedSpecialSuccessRange: IntRange = EXPECTED[skill]!![1],
expectedSuccessRange: IntRange = EXPECTED[skill]!![2],
expectedFailureRange: IntRange = EXPECTED[skill]!![3],
expectedCriticalFailureRange: IntRange = EXPECTED[skill]!![4],
) {
val step = SkillStepUseCase.computeSkillStep(skill = skill)
assert(step.criticalSuccessRange == expectedCriticalSuccessRange) {
"Skill level $skill - Critical success range should be $expectedCriticalSuccessRange bu was ${step.criticalSuccessRange}"
}
assert(step.specialSuccessRange == expectedSpecialSuccessRange) {
"Skill level $skill - Special success range should be $expectedSpecialSuccessRange bu was ${step.specialSuccessRange}"
}
assert(step.successRange == expectedSuccessRange) {
"Skill level $skill - Success range should be $expectedSuccessRange bu was ${step.successRange}"
}
assert(step.failureRange == expectedFailureRange) {
"Skill level $skill - failure range should be $expectedFailureRange bu was ${step.failureRange}"
}
assert(step.criticalFailureRange == expectedCriticalFailureRange) {
"Skill level $skill - Critical failure range should be $expectedCriticalFailureRange bu was ${step.criticalFailureRange}"
}
}
}

View file

@ -1,35 +1,11 @@
package com.pixelized.desktop.lwa
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Button
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Edit
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.rememberWindowState
import androidx.lifecycle.viewmodel.compose.viewModel
import com.pixelized.desktop.lwa.screen.characterSheet.CharacterSheet
import com.pixelized.desktop.lwa.screen.characterSheet.CharacterSheetUio
import com.pixelized.desktop.lwa.screen.characterSheet.CharacterSheetViewModel
import com.pixelized.desktop.lwa.screen.characterSheet.edit.CharacterSheetEdit
import com.pixelized.desktop.lwa.screen.characterSheet.edit.CharacterSheetEditUio
import com.pixelized.desktop.lwa.screen.characterSheet.edit.FieldUio
import com.pixelized.desktop.lwa.screen.overlay.BlurOverlay
import com.pixelized.desktop.lwa.screen.overlay.BlurOverlayViewModel
import com.pixelized.desktop.lwa.screen.roll.RollPage
import com.pixelized.desktop.lwa.screen.roll.RollViewModel
import com.pixelized.desktop.lwa.navigation.MainNavHost
import com.pixelized.desktop.lwa.screen.main.MainPage
import com.pixelized.desktop.lwa.theme.LwaTheme
import org.jetbrains.compose.ui.tooling.preview.Preview
@ -40,120 +16,128 @@ fun App() {
Surface(
modifier = Modifier.fillMaxSize()
) {
val sheetViewModel = viewModel { CharacterSheetViewModel() }
val overlayViewModel = viewModel { BlurOverlayViewModel() }
val rollViewModel = viewModel { RollViewModel() }
val edit = remember { mutableStateOf<CharacterSheetEditUio?>(null) }
Column(
modifier = Modifier.padding(all = 16.dp),
) {
Row {
Button(
onClick = sheetViewModel::showCharacterSheet,
) {
Text(text = "Koryas Tissenpa")
}
IconButton(
onClick = {
edit.value = CharacterSheetEditUio.create(
sheet = CharacterSheetUio.Koryas,
)
}
) {
Icon(
imageVector = Icons.Default.Edit,
contentDescription = null
)
}
}
Button(
onClick = { edit.value = CharacterSheetEditUio.Default },
) {
Text(text = "Créer une feuille de personnage")
}
}
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()
}
)
},
)
}
}
}
edit.value?.let { sheet ->
Window(
onCloseRequest = { edit.value = null },
state = rememberWindowState(
width = 320.dp + 64.dp,
height = 900.dp,
),
title = "LwaCharacterSheet",
) {
Surface(
modifier = Modifier.fillMaxSize(),
) {
CharacterSheetEdit(
form = sheet,
onSkill = { skill ->
edit.value = sheet.copy(
groups = sheet.groups.map { group ->
if (skill.title == group.title) {
group.copy(
fields = mutableListOf<FieldUio>().apply {
addAll(group.fields)
add(
FieldUio.create(
label = "",
valuePlaceHolder = { "40" },
)
)
}
)
} else {
group
}
}
)
}
)
}
}
}
MainNavHost()
}
}
}
//
// val sheetViewModel = viewModel { CharacterSheetViewModel() }
// val overlayViewModel = viewModel { BlurOverlayViewModel() }
// val rollViewModel = viewModel { RollViewModel() }
//
// val edit = remember { mutableStateOf<CharacterSheetEditUio?>(null) }
//
//
//
// Column(
// modifier = Modifier.padding(all = 16.dp),
// ) {
// Row {
// Button(
// onClick = sheetViewModel::showCharacterSheet,
// ) {
// Text(text = "Koryas Tissenpa")
// }
// IconButton(
// onClick = {
// edit.value = CharacterSheetEditUio.create(
// sheet = CharacterSheetUio.Koryas,
// )
// }
// ) {
// Icon(
// imageVector = Icons.Default.Edit,
// contentDescription = null
// )
// }
// }
// Button(
// onClick = { edit.value = CharacterSheetEditUio.Default },
// ) {
// Text(text = "Créer une feuille de personnage")
// }
// }
//
// 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()
// }
// )
// },
// )
// }
// }
// }
//
// edit.value?.let { sheet ->
// Window(
// onCloseRequest = { edit.value = null },
// state = rememberWindowState(
// width = 320.dp + 64.dp,
// height = 900.dp,
// ),
// title = "LwaCharacterSheet",
// ) {
// Surface(
// modifier = Modifier.fillMaxSize(),
// ) {
// CharacterSheetEdit(
// form = sheet,
// onSkill = { skill ->
// edit.value = sheet.copy(
// groups = sheet.groups.map { group ->
// if (skill.title == group.title) {
// group.copy(
// fields = mutableListOf<FieldUio>().apply {
// addAll(group.fields)
// add(
// FieldUio.create(
// label = "",
// valuePlaceHolder = { "40" },
// )
// )
// }
// )
// } else {
// group
// }
// }
// )
// }
// )
// }
// }
// }
// }
// }
//}

View file

@ -1,4 +1,4 @@
package com.pixelized.desktop.lwa.composable
package com.pixelized.desktop.lwa.composable.decoratedBox
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box

View file

@ -1,4 +1,4 @@
package com.pixelized.desktop.lwa.screen.overlay
package com.pixelized.desktop.lwa.composable.overlay
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.fadeIn
@ -10,21 +10,37 @@ 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.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.blur
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.lifecycle.viewmodel.compose.viewModel
@Stable
data class BlurOverlayTransitionUio(
val blur: State<Dp>,
val background: State<Color>,
)
@Composable
fun BlurOverlay(
viewModel: BlurOverlayViewModel = viewModel { BlurOverlayViewModel() },
overlay: @Composable BoxScope.() -> Unit,
content: @Composable BoxScope.() -> Unit,
) {
val transition = viewModel.transition
Box {
Box(
modifier = Modifier
.fillMaxSize()
.blur(radius = viewModel.blur.value),
.blur(radius = transition.blur.value)
.drawWithContent {
drawContent()
drawRect(color = transition.background.value)
},
content = content,
)

View file

@ -0,0 +1,50 @@
package com.pixelized.desktop.lwa.composable.overlay
import androidx.compose.animation.animateColor
import androidx.compose.animation.core.animateDp
import androidx.compose.animation.core.updateTransition
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color
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 transition: BlurOverlayTransitionUio
@Composable
get() {
val transition = updateTransition(_overlay.value)
val blur = transition.animateDp {
when (it) {
true -> 8.dp
else -> 0.dp
}
}
val background = transition.animateColor {
when (it) {
true -> Color.Black.copy(alpha = 0.6f)
else -> Color.Black.copy(alpha = 0f)
}
}
return remember {
BlurOverlayTransitionUio(
blur = blur,
background = background,
)
}
}
fun show() {
_overlay.value = true
}
fun hide() {
_overlay.value = false
}
}

View file

@ -1,13 +1,21 @@
package com.pixelized.desktop.lwa
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
title = "LwaCharacterSheet",
) {
App()
fun main() {
application {
Window(
onCloseRequest = ::exitApplication,
state = rememberWindowState(
width = 320.dp + 64.dp,
height = 900.dp,
),
title = "LwaCharacterSheet",
) {
App()
}
}
}

View file

@ -0,0 +1,34 @@
package com.pixelized.desktop.lwa.navigation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.compositionLocalOf
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.rememberNavController
import com.pixelized.desktop.lwa.navigation.destination.MainDestination
import com.pixelized.desktop.lwa.navigation.destination.composableCharacterSheetEditPage
import com.pixelized.desktop.lwa.navigation.destination.composableMainPage
val LocalScreen = compositionLocalOf<NavHostController> {
error("MainNavHost controller is not yet ready")
}
@Composable
fun MainNavHost(
controller: NavHostController = rememberNavController(),
startDestination: String = MainDestination.navigationRoute(),
) {
CompositionLocalProvider(
LocalScreen provides controller,
) {
NavHost(
navController = controller,
startDestination = startDestination,
) {
composableMainPage()
composableCharacterSheetEditPage()
}
}
}

View file

@ -0,0 +1,27 @@
package com.pixelized.desktop.lwa.navigation.destination
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import com.pixelized.desktop.lwa.screen.characterSheet.edit.CharacterSheetEdit
import com.pixelized.desktop.lwa.screen.characterSheet.edit.CharacterSheetEditPage
object CharacterSheetEditDestination {
private const val ROUTE = "character.sheet.edit"
fun baseRoute() = ROUTE
fun navigationRoute() = ROUTE
}
fun NavGraphBuilder.composableCharacterSheetEditPage() {
composable(
route = CharacterSheetEditDestination.baseRoute(),
) {
CharacterSheetEditPage()
}
}
fun NavHostController.navigateToCharacterSheetEdit() {
val route = CharacterSheetEditDestination.navigationRoute()
navigate(route = route)
}

View file

@ -0,0 +1,26 @@
package com.pixelized.desktop.lwa.navigation.destination
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import com.pixelized.desktop.lwa.screen.main.MainPage
object MainDestination {
private const val ROUTE = "main"
fun baseRoute() = ROUTE
fun navigationRoute() = ROUTE
}
fun NavGraphBuilder.composableMainPage() {
composable(
route = MainDestination.baseRoute(),
) {
MainPage()
}
}
fun NavHostController.navigateToMainPage() {
val route = MainDestination.navigationRoute()
navigate(route = route)
}

View file

@ -15,7 +15,6 @@ 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
@ -28,11 +27,11 @@ 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
import com.pixelized.desktop.lwa.composable.decoratedBox.DecoratedBox
import com.pixelized.desktop.lwa.screen.characterSheet.CharacterSheetPageUio.Node.Type
@Stable
data class CharacterSheetUio(
data class CharacterSheetPageUio(
val name: String,
val characteristics: List<Characteristic>,
val subCharacteristics: List<Characteristic>,
@ -61,7 +60,7 @@ data class CharacterSheetUio(
}
companion object {
val Koryas = CharacterSheetUio(
val Koryas = CharacterSheetPageUio(
name = "Koryas Tissenpa",
characteristics = listOf(
Characteristic(label = "Force", value = "10"),
@ -113,13 +112,13 @@ data class CharacterSheetUio(
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun CharacterSheet(
fun CharacterSheetPage(
modifier: Modifier,
scrollState: ScrollState = rememberScrollState(),
width: Dp = 320.dp,
characterSheet: CharacterSheetUio = CharacterSheetUio.Koryas,
onCharacteristic: (characteristic: CharacterSheetUio.Characteristic) -> Unit,
onSkill: (skill: CharacterSheetUio.Node) -> Unit,
characterSheet: CharacterSheetPageUio = CharacterSheetPageUio.Koryas,
onCharacteristic: (characteristic: CharacterSheetPageUio.Characteristic) -> Unit,
onSkill: (skill: CharacterSheetPageUio.Node) -> Unit,
) {
Column(
modifier = Modifier
@ -247,7 +246,7 @@ fun CharacterSheet(
private fun Stat(
modifier: Modifier = Modifier,
paddingValues: PaddingValues = PaddingValues(all = 8.dp),
characteristic: CharacterSheetUio.Characteristic,
characteristic: CharacterSheetPageUio.Characteristic,
onClick: () -> Unit,
) {
DecoratedBox(
@ -277,7 +276,7 @@ private fun Stat(
private fun Characteristics(
modifier: Modifier = Modifier,
paddingValues: PaddingValues = PaddingValues(horizontal = 8.dp),
characteristic: CharacterSheetUio.Characteristic,
characteristic: CharacterSheetPageUio.Characteristic,
) {
Row(
modifier = Modifier

View file

@ -6,11 +6,11 @@ import androidx.lifecycle.ViewModel
class CharacterSheetViewModel : ViewModel() {
private val _sheet = mutableStateOf<CharacterSheetUio?>(null)
val sheet: State<CharacterSheetUio?> get() = _sheet
private val _sheet = mutableStateOf<CharacterSheetPageUio?>(null)
val sheet: State<CharacterSheetPageUio?> get() = _sheet
fun showCharacterSheet() {
_sheet.value = CharacterSheetUio.Koryas
_sheet.value = CharacterSheetPageUio.Koryas
}
fun hideCharacterSheet() {

View file

@ -1,323 +0,0 @@
package com.pixelized.desktop.lwa.screen.characterSheet.edit
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.composable.DecoratedBox
import com.pixelized.desktop.lwa.screen.characterSheet.CharacterSheetUio
import kotlin.math.max
import kotlin.math.truncate
@Stable
data class CharacterSheetEditUio(
val name: FieldUio,
val groups: List<Group>,
) {
@Stable
data class Group(
val title: String,
val editable: Boolean = false,
val fields: List<FieldUio>,
)
companion object {
val Default = run {
val strField = FieldUio.create(label = "Force", valuePlaceHolder = { "0" })
fun str(): Int = strField.value.value.toIntOrNull() ?: 0
val dexField = FieldUio.create(label = "Dextérité", valuePlaceHolder = { "0" })
fun dex(): Int = dexField.value.value.toIntOrNull() ?: 0
val conField = FieldUio.create(label = "Constitution", valuePlaceHolder = { "0" })
fun con(): Int = conField.value.value.toIntOrNull() ?: 0
val vitField = FieldUio.create(label = "Taille", valuePlaceHolder = { "0" })
fun vit(): Int = vitField.value.value.toIntOrNull() ?: 0
val intField = FieldUio.create(label = "Intelligence", valuePlaceHolder = { "0" })
fun int(): Int = intField.value.value.toIntOrNull() ?: 0
val powField = FieldUio.create(label = "Pouvoir", valuePlaceHolder = { "0" })
fun pow(): Int = powField.value.value.toIntOrNull() ?: 0
val chaField = FieldUio.create(label = "Charisme", valuePlaceHolder = { "0" })
fun cha(): Int = chaField.value.value.toIntOrNull() ?: 0
CharacterSheetEditUio(
name = FieldUio.create(
useLabelAsPlaceholder = true,
label = "Name",
),
groups = listOf(
Group(
title = "Charactéristiques",
fields = listOf(
strField,
dexField,
conField,
vitField,
intField,
powField,
chaField
),
),
Group(
title = "Charactéristiques dérivées",
fields = listOf(
FieldUio.create(
label = "Déplacement",
valuePlaceHolder = { "10" },
),
FieldUio.create(
label = "Points de vie",
valuePlaceHolder = { "${(con() + vit()) / 2}" },
),
FieldUio.create(
label = "Points de pouvoir",
valuePlaceHolder = { "${pow()}" },
),
FieldUio.create(
label = "Bonus aux dégats",
valuePlaceHolder = {
val bonus = str() + vit()
when {
bonus < 12 -> "-1d6"
bonus in 12..17 -> "-1d4"
bonus in 18..22 -> "-0"
bonus in 23..29 -> "1d4"
bonus in 30..39 -> "1d6"
else -> "2d6"
}
},
),
FieldUio.create(label = "Armure", valuePlaceHolder = { "0" }),
),
),
Group(
title = "Compétances",
editable = true,
fields = listOf(
FieldUio.create(
label = "Bagarre",
valuePlaceHolder = { trunc(dex() * 2) },
),
FieldUio.create(
label = "Esquive",
valuePlaceHolder = { trunc(dex() * 2) }
),
FieldUio.create(
label = "Saisie",
valuePlaceHolder = { trunc(str() + vit()) },
),
FieldUio.create(
label = "Lancer",
valuePlaceHolder = { trunc(str() + dex()) },
),
FieldUio.create(
label = "Athlétisme",
valuePlaceHolder = { trunc(str() + con() * 2) },
),
FieldUio.create(
label = "Acrobatie",
valuePlaceHolder = { trunc(dex() + con() * 2) },
),
FieldUio.create(
label = "Perception",
valuePlaceHolder = { trunc(10 + int() * 2) },
),
FieldUio.create(
label = "Recherche",
valuePlaceHolder = { trunc(10 + int() * 2) },
),
FieldUio.create(
label = "Empathie",
valuePlaceHolder = { trunc(cha() + int()) },
),
FieldUio.create(
label = "Persuasion",
valuePlaceHolder = { trunc(cha() * 3) },
),
FieldUio.create(
label = "Intimidation",
valuePlaceHolder = { trunc(cha() + max(pow(), vit()) * 2) },
),
FieldUio.create(
label = "Baratin",
valuePlaceHolder = { trunc(cha() * 2 + int()) },
),
FieldUio.create(
label = "Marchandage",
valuePlaceHolder = { trunc(cha() * 2) },
),
FieldUio.create(
label = "Discrétion",
valuePlaceHolder = { trunc(cha() + dex() * 2 - vit()) },
),
FieldUio.create(
label = "Escamotage",
valuePlaceHolder = { trunc(dex() * 2) },
),
FieldUio.create(
label = "Premiers soins",
valuePlaceHolder = { trunc(int() + dex()) },
),
),
),
Group(
title = "Occupations",
editable = true,
fields = emptyList(),
),
Group(
title = "Compétences magiques",
editable = true,
fields = emptyList(),
),
)
)
}
fun create(sheet: CharacterSheetUio): CharacterSheetEditUio {
return CharacterSheetEditUio(
name = FieldUio.create(
useLabelAsPlaceholder = true,
label = "Name",
initialValue = sheet.name,
),
groups = listOf(
Group(
title = "Charactéristiques",
fields = sheet.characteristics.map {
FieldUio.create(
label = it.label,
initialValue = it.value,
)
},
),
Group(
title = "Charactéristiques dérivées",
fields = sheet.subCharacteristics.map {
FieldUio.create(
label = it.label,
initialValue = it.value,
)
},
),
Group(
title = "Compétances",
fields = sheet.skills.map {
FieldUio.create(
label = it.label,
initialValue = "${it.value}",
)
},
),
Group(
title = "Occupations",
editable = true,
fields = sheet.occupations.map {
FieldUio.create(
label = it.label,
initialValue = "${it.value}",
)
},
),
Group(
title = "Compétences magiques",
editable = true,
fields = sheet.magics.map {
FieldUio.create(
label = it.label,
initialValue = "${it.value}",
)
},
),
)
)
}
}
}
private fun trunc(value: Int): String {
return "${(truncate(value.toFloat() / 5f) * 5f).toInt()}"
}
@Composable
fun CharacterSheetEdit(
form: CharacterSheetEditUio,
onSkill: (CharacterSheetEditUio.Group) -> Unit,
) {
Column(
modifier = Modifier
.verticalScroll(state = rememberScrollState())
.padding(all = 24.dp),
verticalArrangement = Arrangement.spacedBy(space = 16.dp)
) {
Form(
modifier = Modifier.fillMaxWidth(),
field = form.name,
)
form.groups.forEach {
DecoratedBox(
modifier = Modifier.animateContentSize(),
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
modifier = Modifier.padding(vertical = 8.dp),
style = MaterialTheme.typography.caption,
text = it.title,
)
it.fields.forEach {
Form(
modifier = Modifier.fillMaxWidth(),
field = it,
)
}
if (it.editable) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(
space = 8.dp,
alignment = Alignment.End
)
) {
Button(
colors = ButtonDefaults.textButtonColors(),
onClick = { onSkill(it) },
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
) {
Text(
style = MaterialTheme.typography.caption,
text = "Ajouter une ligne",
)
Icon(
imageVector = Icons.Default.Add,
contentDescription = null,
)
}
}
}
}
}
}
}
}
}

View file

@ -0,0 +1,166 @@
package com.pixelized.desktop.lwa.screen.characterSheet.edit
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.pixelized.desktop.lwa.composable.decoratedBox.DecoratedBox
import com.pixelized.desktop.lwa.navigation.LocalScreen
import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.FieldUio
import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.Form
import kotlinx.coroutines.launch
@Stable
data class CharacterSheetEditPageUio(
val id: String,
val name: FieldUio,
val skills: List<SkillGroup>,
) {
@Stable
data class SkillGroup(
val title: String,
val type: Type,
val editable: Boolean = false,
val fields: List<FieldUio>,
) {
@Stable
enum class Type {
CHARACTERISTICS,
SUB_CHARACTERISTICS,
SKILLS,
OCCUPATIONS,
MAGICS,
OTHER,
}
}
}
@Composable
fun CharacterSheetEditPage(
viewModel: CharacterSheetEditViewModel = viewModel { CharacterSheetEditViewModel() },
) {
val screen = LocalScreen.current
val scope = rememberCoroutineScope()
Surface(
modifier = Modifier.fillMaxSize(),
) {
CharacterSheetEdit(
form = viewModel.characterSheet.value,
onSkill = viewModel::onSkill,
onCancel = { screen.popBackStack() },
onSave = {
scope.launch {
viewModel.save()
screen.popBackStack()
}
},
)
}
}
@Composable
fun CharacterSheetEdit(
form: CharacterSheetEditPageUio,
onSkill: (CharacterSheetEditPageUio.SkillGroup) -> Unit,
onCancel: () -> Unit,
onSave: () -> Unit,
) {
Column(
modifier = Modifier
.verticalScroll(state = rememberScrollState())
.padding(all = 24.dp),
verticalArrangement = Arrangement.spacedBy(space = 16.dp)
) {
Form(
modifier = Modifier.fillMaxWidth(),
field = form.name,
)
form.skills.forEach {
DecoratedBox(
modifier = Modifier.animateContentSize(),
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
modifier = Modifier.padding(vertical = 8.dp),
style = MaterialTheme.typography.caption,
text = it.title,
)
it.fields.forEach {
Form(
modifier = Modifier.fillMaxWidth(),
field = it,
)
}
if (it.editable) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(
space = 8.dp,
alignment = Alignment.End
)
) {
Button(
colors = ButtonDefaults.textButtonColors(),
onClick = { onSkill(it) },
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
) {
Text(
style = MaterialTheme.typography.caption,
text = "Ajouter une ligne",
)
Icon(
imageVector = Icons.Default.Add,
contentDescription = null,
)
}
}
}
}
}
}
}
Row {
TextButton(
onClick = onCancel,
) {
Text(text = "Annuler")
}
TextButton(
onClick = onSave,
) {
Text(text = "Sauvegarder")
}
}
}
}

View file

@ -0,0 +1,223 @@
package com.pixelized.desktop.lwa.screen.characterSheet.edit
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import com.pixelized.desktop.lwa.business.SkillNormalizerUseCase.normalize
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.desktop.lwa.screen.characterSheet.edit.CharacterSheetEditPageUio.SkillGroup
import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.FieldUio
import java.util.UUID
import kotlin.math.max
class CharacterSheetEditViewModel : ViewModel() {
private val repository = CharacterSheetRepository
private val factory = CharacterSheetFactory()
private val _characterSheet: MutableState<CharacterSheetEditPageUio>
val characterSheet: State<CharacterSheetEditPageUio> get() = _characterSheet
init {
_characterSheet = mutableStateOf(createEmptyCharacterSheet())
}
fun onSkill(skill: SkillGroup) {
val sheet = _characterSheet.value
_characterSheet.value = sheet.copy(
skills = sheet.skills.map { group ->
if (skill.title == group.title) {
group.copy(
fields = mutableListOf<FieldUio>().apply {
addAll(group.fields)
add(
FieldUio.create(
label = "",
isLabelEditable = true,
valuePlaceHolder = {
when (group.type) {
SkillGroup.Type.CHARACTERISTICS -> ""
SkillGroup.Type.SUB_CHARACTERISTICS -> ""
SkillGroup.Type.SKILLS -> "0"
SkillGroup.Type.OCCUPATIONS -> "40"
SkillGroup.Type.MAGICS -> "0"
SkillGroup.Type.OTHER -> ""
}
},
)
)
}
)
} else {
group
}
}
)
}
suspend fun save() {
val sheet = _characterSheet.value
val model = factory.convertToModel(sheet = sheet)
repository.save(characterSheet = model)
}
private fun createEmptyCharacterSheet(): CharacterSheetEditPageUio {
val str = FieldUio.create(label = "Force", valuePlaceHolder = { "0" })
val dex = FieldUio.create(label = "Dextérité", valuePlaceHolder = { "0" })
val con = FieldUio.create(label = "Constitution", valuePlaceHolder = { "0" })
val vit = FieldUio.create(label = "Taille", valuePlaceHolder = { "0" })
val int = FieldUio.create(label = "Intelligence", valuePlaceHolder = { "0" })
val pow = FieldUio.create(label = "Pouvoir", valuePlaceHolder = { "0" })
val cha = FieldUio.create(label = "Charisme", valuePlaceHolder = { "0" })
fun str(): Int = str.value.value.toIntOrNull() ?: 0
fun dex(): Int = dex.value.value.toIntOrNull() ?: 0
fun con(): Int = con.value.value.toIntOrNull() ?: 0
fun vit(): Int = vit.value.value.toIntOrNull() ?: 0
fun int(): Int = int.value.value.toIntOrNull() ?: 0
fun pow(): Int = pow.value.value.toIntOrNull() ?: 0
fun cha(): Int = cha.value.value.toIntOrNull() ?: 0
return CharacterSheetEditPageUio(
id = UUID.randomUUID().toString(),
name = FieldUio.create(
useLabelAsPlaceholder = true,
label = "Name",
),
skills = listOf(
SkillGroup(
title = "Charactéristiques",
type = SkillGroup.Type.CHARACTERISTICS,
fields = listOf(str, dex, con, vit, int, pow, cha),
),
SkillGroup(
title = "Charactéristiques dérivées",
type = SkillGroup.Type.SUB_CHARACTERISTICS,
fields = listOf(
FieldUio.create(
label = "Déplacement",
valuePlaceHolder = { "10" },
),
FieldUio.create(
label = "Points de vie",
valuePlaceHolder = { "${(con() + vit()) / 2}" },
),
FieldUio.create(
label = "Points de pouvoir",
valuePlaceHolder = { "${pow()}" },
),
FieldUio.create(
label = "Bonus aux dégats",
valuePlaceHolder = {
val bonus = str() + vit()
when {
bonus < 12 -> "-1d6"
bonus in 12..17 -> "-1d4"
bonus in 18..22 -> "-0"
bonus in 23..29 -> "1d4"
bonus in 30..39 -> "1d6"
else -> "2d6"
}
},
),
FieldUio.create(label = "Armure", valuePlaceHolder = { "0" }),
),
),
SkillGroup(
title = "Compétances",
type = SkillGroup.Type.SKILLS,
editable = true,
fields = listOf(
FieldUio.create(
label = "Bagarre",
valuePlaceHolder = { "${normalize(dex() * 2)}" },
),
FieldUio.create(
label = "Esquive",
valuePlaceHolder = { "${normalize(dex() * 2)}" }
),
FieldUio.create(
label = "Saisie",
valuePlaceHolder = { "${normalize(str() + vit())}" },
),
FieldUio.create(
label = "Lancer",
valuePlaceHolder = { "${normalize(str() + dex())}" },
),
FieldUio.create(
label = "Athlétisme",
valuePlaceHolder = { "${normalize(str() + con() * 2)}" },
),
FieldUio.create(
label = "Acrobatie",
valuePlaceHolder = { "${normalize(dex() + con() * 2)}" },
),
FieldUio.create(
label = "Perception",
valuePlaceHolder = { "${normalize(10 + int() * 2)}" },
),
FieldUio.create(
label = "Recherche",
valuePlaceHolder = { "${normalize(10 + int() * 2)}" },
),
FieldUio.create(
label = "Empathie",
valuePlaceHolder = { "${normalize(cha() + int())}" },
),
FieldUio.create(
label = "Persuasion",
valuePlaceHolder = { "${normalize(cha() * 3)}" },
),
FieldUio.create(
label = "Intimidation",
valuePlaceHolder = {
"${
normalize(
cha() + max(
pow(),
vit()
) * 2
)
}"
},
),
FieldUio.create(
label = "Baratin",
valuePlaceHolder = { "${normalize(cha() * 2 + int())}" },
),
FieldUio.create(
label = "Marchandage",
valuePlaceHolder = { "${normalize(cha() * 2)}" },
),
FieldUio.create(
label = "Discrétion",
valuePlaceHolder = { "${normalize(cha() + dex() * 2 - vit())}" },
),
FieldUio.create(
label = "Escamotage",
valuePlaceHolder = { "${normalize(dex() * 2)}" },
),
FieldUio.create(
label = "Premiers soins",
valuePlaceHolder = { "${normalize(int() + dex())}" },
),
),
),
SkillGroup(
title = "Occupations",
type = SkillGroup.Type.OCCUPATIONS,
editable = true,
fields = emptyList(),
),
SkillGroup(
title = "Compétences magiques",
type = SkillGroup.Type.MAGICS,
editable = true,
fields = emptyList(),
),
)
)
}
}

View file

@ -0,0 +1,58 @@
package com.pixelized.desktop.lwa.screen.characterSheet.edit
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheet
import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.FieldUio
class CharacterSheetFactory {
fun convertToModel(sheet: CharacterSheetEditPageUio): CharacterSheet {
return CharacterSheet(
id = sheet.id,
name = sheet.name.value.value,
strength = sheet.skills[0].fields[0].unpack(),
dexterity = sheet.skills[0].fields[1].unpack(),
constitution = sheet.skills[0].fields[2].unpack(),
height = sheet.skills[0].fields[3].unpack(),
intelligence = sheet.skills[0].fields[4].unpack(),
power = sheet.skills[0].fields[5].unpack(),
charisma = sheet.skills[0].fields[6].unpack(),
movement = sheet.skills[1].fields[0].unpack(),
currentHp = sheet.skills[1].fields[1].unpack(),
maxHp = sheet.skills[1].fields[1].unpack(),
currentPP = sheet.skills[1].fields[2].unpack(),
maxPP = sheet.skills[1].fields[2].unpack(),
damageBonus = sheet.skills[1].fields[3].unpack(),
armor = sheet.skills[1].fields[4].unpack(),
skills = sheet.skills[2].fields.map {
CharacterSheet.Skill(
label = it.label.value,
value = it.value.value.toIntOrNull() ?: 0,
used = false,
)
},
occupations = sheet.skills[3].fields.map {
CharacterSheet.Skill(
label = it.label.value,
value = it.value.value.toIntOrNull() ?: 0,
used = false,
)
},
magics = sheet.skills[4].fields.map {
CharacterSheet.Skill(
label = it.label.value,
value = it.value.value.toIntOrNull() ?: 0,
used = false,
)
},
attacks = emptyList(),
)
}
private inline fun <reified T> FieldUio.unpack(): T {
val tmp = value.value.ifBlank { valuePlaceHolder.value }
return when (T::class) {
Int::class -> (tmp.toIntOrNull() ?: 0) as T
else -> tmp as T
}
}
}

View file

@ -1,4 +1,4 @@
package com.pixelized.desktop.lwa.screen.characterSheet.edit
package com.pixelized.desktop.lwa.screen.characterSheet.edit.composable
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.fadeIn
@ -6,8 +6,10 @@ import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.runtime.Composable
@ -19,11 +21,13 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@Stable
open class FieldUio(
val useLabelAsPlaceholder: Boolean,
val isLabelEditable: Boolean,
val label: State<String>,
val onLabelChange: (String) -> Unit,
val valuePlaceHolder: State<String>,
@ -34,6 +38,7 @@ open class FieldUio(
@Stable
fun create(
useLabelAsPlaceholder: Boolean = false,
isLabelEditable: Boolean = false,
label: String = "",
initialValue: String = "",
valuePlaceHolder: () -> String = { "" },
@ -42,6 +47,7 @@ open class FieldUio(
val valueState = mutableStateOf(initialValue)
return FieldUio(
useLabelAsPlaceholder = useLabelAsPlaceholder,
isLabelEditable = useLabelAsPlaceholder.not() && isLabelEditable,
label = labelState,
onLabelChange = { labelState.value = it },
valuePlaceHolder = derivedStateOf(valuePlaceHolder),
@ -81,14 +87,29 @@ fun Form(
horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
TextField(
AnimatedContent(
modifier = Modifier.weight(weight = 1f),
value = field.label.value,
placeholder = { Text(text = "Nom") },
singleLine = true,
keyboardActions = KeyboardActions { focus.moveFocus(FocusDirection.Next) },
onValueChange = field.onLabelChange,
)
targetState = field.isLabelEditable,
transitionSpec = { fadeIn() togetherWith fadeOut() },
) { editable ->
when (editable) {
true -> TextField(
value = field.label.value,
placeholder = { Text(text = "Nom") },
singleLine = true,
keyboardActions = KeyboardActions { focus.moveFocus(FocusDirection.Next) },
onValueChange = field.onLabelChange,
)
else -> Text(
modifier = Modifier.padding(horizontal = 16.dp),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.body1,
text = field.label.value
)
}
}
TextField(
modifier = Modifier.width(width = 80.dp),
value = field.value.value,

View file

@ -0,0 +1,108 @@
package com.pixelized.desktop.lwa.screen.main
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.lifecycle.viewmodel.compose.viewModel
import com.pixelized.desktop.lwa.composable.decoratedBox.DecoratedBox
import com.pixelized.desktop.lwa.navigation.LocalScreen
import com.pixelized.desktop.lwa.navigation.destination.navigateToCharacterSheetEdit
@Stable
data class CharacterUio(
val id: String,
val name: String,
)
@Composable
fun MainPage(
viewModel: MainPageViewModel = viewModel { MainPageViewModel() },
) {
val screen = LocalScreen.current
Surface {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
MainPageContent(
characters = viewModel.characters,
onCharacter = {
},
onCharacterEdit = {
},
onCharacterDelete = {
},
onCreateCharacter = {
screen.navigateToCharacterSheetEdit()
},
)
}
}
}
@Composable
fun MainPageContent(
modifier: Modifier = Modifier,
characters: State<List<CharacterUio>>,
onCharacter: (CharacterUio) -> Unit,
onCharacterEdit: (CharacterUio) -> Unit,
onCharacterDelete: (CharacterUio) -> Unit,
onCreateCharacter: () -> Unit,
) {
Column(
modifier = modifier,
) {
DecoratedBox {
Column {
characters.value.forEach { sheet ->
Row {
TextButton(
onClick = { onCharacter(sheet) },
) {
Text(text = sheet.name)
}
IconButton(
onClick = { onCharacterEdit(sheet) },
) {
Icon(
imageVector = Icons.Default.Edit,
contentDescription = null,
)
}
IconButton(
onClick = { onCharacterDelete(sheet) },
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = null,
)
}
}
}
TextButton(
onClick = { onCreateCharacter() },
) {
Text(text = "Créer une feuille de personnage")
}
}
}
}
}

View file

@ -0,0 +1,29 @@
package com.pixelized.desktop.lwa.screen.main
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.lifecycle.ViewModel
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.desktop.lwa.utils.extention.collectAsState
class MainPageViewModel : ViewModel() {
// using a variable to help with later injection.
private val characterSheetRepository = CharacterSheetRepository
val characters: State<List<CharacterUio>>
@Composable
@Stable
get() {
return characterSheetRepository.characterSheet().collectAsState { sheets ->
sheets.map { sheet ->
CharacterUio(
id = sheet.id,
name = sheet.name,
)
}
}
}
}

View file

@ -1,33 +0,0 @@
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

@ -8,7 +8,7 @@ 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 com.pixelized.desktop.lwa.screen.characterSheet.CharacterSheetPageUio
import kotlinx.coroutines.Job
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
@ -26,7 +26,7 @@ class RollViewModel : ViewModel() {
private val _result = mutableStateOf<RollResultUio?>(null)
val result: State<RollResultUio?> get() = _result
fun prepareRoll(node: CharacterSheetUio.Node) {
fun prepareRoll(node: CharacterSheetPageUio.Node) {
val step = SkillStepUseCase.computeSkillStep(
skill = node.value,
)
@ -36,7 +36,7 @@ class RollViewModel : ViewModel() {
)
}
fun prepareRoll(characteristic: CharacterSheetUio.Characteristic) {
fun prepareRoll(characteristic: CharacterSheetPageUio.Characteristic) {
val step = SkillStepUseCase.computeSkillStep(
skill = (characteristic.value.toIntOrNull() ?: 0) * 5
)

View file

@ -1,16 +1,23 @@
[versions]
androidx-lifecycle = "2.8.3"
compose-multiplatform = "1.7.0"
junit = "4.13.2"
kotlin = "2.0.21"
kotlinx-coroutines = "1.9.0"
junit = "4.13.2"
compose-multiplatform = "1.7.0"
androidx-lifecycle = "2.8.3"
androidx-navigation = "2.8.0-alpha10"
androidx-datastore = "1.1.1"
[libraries]
# Test
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
# Compose
androidx-lifecycle-viewmodel = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "androidx-lifecycle" }
androidx-lifecycle-runtime-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
androidx-navigation-compose = { group = "org.jetbrains.androidx.navigation", name = "navigation-compose", version.ref = "androidx-navigation" }
androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences-core", version.ref = "androidx-datastore" }
kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
[plugins]