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:
		
							parent
							
								
									d74a5fcd7c
								
							
						
					
					
						commit
						65aa53890f
					
				
					 34 changed files with 1412 additions and 541 deletions
				
			
		|  | @ -8,10 +8,10 @@ plugins { | ||||||
| 
 | 
 | ||||||
| kotlin { | kotlin { | ||||||
|     jvm("desktop") |     jvm("desktop") | ||||||
|      | 
 | ||||||
|     sourceSets { |     sourceSets { | ||||||
|         val desktopMain by getting |         val desktopMain by getting | ||||||
|          | 
 | ||||||
|         commonMain.dependencies { |         commonMain.dependencies { | ||||||
|             implementation(compose.runtime) |             implementation(compose.runtime) | ||||||
|             implementation(compose.foundation) |             implementation(compose.foundation) | ||||||
|  | @ -21,9 +21,14 @@ kotlin { | ||||||
|             implementation(compose.components.uiToolingPreview) |             implementation(compose.components.uiToolingPreview) | ||||||
|             implementation(libs.androidx.lifecycle.viewmodel) |             implementation(libs.androidx.lifecycle.viewmodel) | ||||||
|             implementation(libs.androidx.lifecycle.runtime.compose) |             implementation(libs.androidx.lifecycle.runtime.compose) | ||||||
| 
 |             implementation(libs.androidx.navigation.compose) | ||||||
|             implementation("org.jetbrains.androidx.navigation:navigation-compose:2.8.0-alpha10") |             implementation(libs.androidx.datastore.preferences) | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|  |         commonTest.dependencies { | ||||||
|  |             implementation(libs.kotlin.test) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         desktopMain.dependencies { |         desktopMain.dependencies { | ||||||
|             implementation(compose.desktop.currentOs) |             implementation(compose.desktop.currentOs) | ||||||
|             implementation(libs.kotlinx.coroutines.swing) |             implementation(libs.kotlinx.coroutines.swing) | ||||||
|  | @ -40,6 +45,9 @@ compose.desktop { | ||||||
|             targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) |             targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) | ||||||
|             packageName = "com.pixelized.desktop.lwa" |             packageName = "com.pixelized.desktop.lwa" | ||||||
|             packageVersion = "1.0.0" |             packageVersion = "1.0.0" | ||||||
|  | 
 | ||||||
|  |             // Use system theming fot the app toolbars. | ||||||
|  |             jvmArgs("-Dapple.awt.application.appearance=system") | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										
											BIN
										
									
								
								composeApp/characterssheet.preferences_pb
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								composeApp/characterssheet.preferences_pb
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							|  | @ -5,10 +5,8 @@ object RollUseCase { | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * (Math.random() * 100 + 1).toInt() |      * (Math.random() * 100 + 1).toInt() | ||||||
|      * TODO : test. |  | ||||||
|      */ |      */ | ||||||
|     fun rollD100(): Int { |     fun rollD100(): Int { | ||||||
|         return d100.random() |         return d100.random() | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
| } | } | ||||||
|  | @ -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() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -61,7 +61,7 @@ object SkillStepUseCase { | ||||||
|             55 -> ((criticalSuccess?.second ?: 0) + 1) to 11 |             55 -> ((criticalSuccess?.second ?: 0) + 1) to 11 | ||||||
|             60 -> ((criticalSuccess?.second ?: 0) + 1) to 12 |             60 -> ((criticalSuccess?.second ?: 0) + 1) to 12 | ||||||
|             65 -> ((criticalSuccess?.second ?: 0) + 1) to 13 |             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 |             75 -> ((criticalSuccess?.second ?: 0) + 1) to 15 | ||||||
|             80 -> ((criticalSuccess?.second ?: 0) + 1) to 16 |             80 -> ((criticalSuccess?.second ?: 0) + 1) to 16 | ||||||
|             85 -> ((criticalSuccess?.second ?: 0) + 1) to 17 |             85 -> ((criticalSuccess?.second ?: 0) + 1) to 17 | ||||||
|  | @ -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() }) | ||||||
|  | } | ||||||
|  | @ -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 | ||||||
|  | } | ||||||
|  | @ -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") | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -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) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | @ -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 | ||||||
|  | } | ||||||
|  | @ -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) } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -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 | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -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" | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -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}" | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,35 +1,11 @@ | ||||||
| package com.pixelized.desktop.lwa | 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.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.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.Composable | ||||||
| import androidx.compose.runtime.mutableStateOf |  | ||||||
| import androidx.compose.runtime.remember |  | ||||||
| import androidx.compose.ui.Modifier | import androidx.compose.ui.Modifier | ||||||
| import androidx.compose.ui.unit.dp | import com.pixelized.desktop.lwa.navigation.MainNavHost | ||||||
| import androidx.compose.ui.window.Window | import com.pixelized.desktop.lwa.screen.main.MainPage | ||||||
| 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.theme.LwaTheme | import com.pixelized.desktop.lwa.theme.LwaTheme | ||||||
| import org.jetbrains.compose.ui.tooling.preview.Preview | import org.jetbrains.compose.ui.tooling.preview.Preview | ||||||
| 
 | 
 | ||||||
|  | @ -40,120 +16,128 @@ fun App() { | ||||||
|         Surface( |         Surface( | ||||||
|             modifier = Modifier.fillMaxSize() |             modifier = Modifier.fillMaxSize() | ||||||
|         ) { |         ) { | ||||||
|             val sheetViewModel = viewModel { CharacterSheetViewModel() } |             MainNavHost() | ||||||
|             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 |  | ||||||
|                                         } |  | ||||||
|                                     } |  | ||||||
|                                 ) |  | ||||||
|                             } |  | ||||||
|                         ) |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | // | ||||||
|  | //            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 | ||||||
|  | //                                        } | ||||||
|  | //                                    } | ||||||
|  | //                                ) | ||||||
|  | //                            } | ||||||
|  | //                        ) | ||||||
|  | //                    } | ||||||
|  | //                } | ||||||
|  | //            } | ||||||
|  | //        } | ||||||
|  | //    } | ||||||
|  | //} | ||||||
|  | @ -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.border | ||||||
| import androidx.compose.foundation.layout.Box | import androidx.compose.foundation.layout.Box | ||||||
|  | @ -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.AnimatedContent | ||||||
| import androidx.compose.animation.fadeIn | 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.BoxScope | ||||||
| import androidx.compose.foundation.layout.fillMaxSize | import androidx.compose.foundation.layout.fillMaxSize | ||||||
| import androidx.compose.runtime.Composable | import androidx.compose.runtime.Composable | ||||||
|  | import androidx.compose.runtime.Stable | ||||||
|  | import androidx.compose.runtime.State | ||||||
| import androidx.compose.ui.Modifier | import androidx.compose.ui.Modifier | ||||||
| import androidx.compose.ui.draw.blur | 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 | import androidx.lifecycle.viewmodel.compose.viewModel | ||||||
| 
 | 
 | ||||||
|  | @Stable | ||||||
|  | data class BlurOverlayTransitionUio( | ||||||
|  |     val blur: State<Dp>, | ||||||
|  |     val background: State<Color>, | ||||||
|  | ) | ||||||
|  | 
 | ||||||
| @Composable | @Composable | ||||||
| fun BlurOverlay( | fun BlurOverlay( | ||||||
|     viewModel: BlurOverlayViewModel = viewModel { BlurOverlayViewModel() }, |     viewModel: BlurOverlayViewModel = viewModel { BlurOverlayViewModel() }, | ||||||
|     overlay: @Composable BoxScope.() -> Unit, |     overlay: @Composable BoxScope.() -> Unit, | ||||||
|     content: @Composable BoxScope.() -> Unit, |     content: @Composable BoxScope.() -> Unit, | ||||||
| ) { | ) { | ||||||
|  |     val transition = viewModel.transition | ||||||
|     Box { |     Box { | ||||||
|         Box( |         Box( | ||||||
|             modifier = Modifier |             modifier = Modifier | ||||||
|                 .fillMaxSize() |                 .fillMaxSize() | ||||||
|                 .blur(radius = viewModel.blur.value), |                 .blur(radius = transition.blur.value) | ||||||
|  |                 .drawWithContent { | ||||||
|  |                     drawContent() | ||||||
|  |                     drawRect(color = transition.background.value) | ||||||
|  |                 }, | ||||||
|             content = content, |             content = content, | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|  | @ -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 | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,13 +1,21 @@ | ||||||
| package com.pixelized.desktop.lwa | package com.pixelized.desktop.lwa | ||||||
| 
 | 
 | ||||||
|  | import androidx.compose.ui.unit.dp | ||||||
| import androidx.compose.ui.window.Window | import androidx.compose.ui.window.Window | ||||||
| import androidx.compose.ui.window.application | import androidx.compose.ui.window.application | ||||||
|  | import androidx.compose.ui.window.rememberWindowState | ||||||
| 
 | 
 | ||||||
| fun main() = application { | fun main() { | ||||||
|     Window( |     application { | ||||||
|         onCloseRequest = ::exitApplication, |         Window( | ||||||
|         title = "LwaCharacterSheet", |             onCloseRequest = ::exitApplication, | ||||||
|     ) { |             state = rememberWindowState( | ||||||
|         App() |                 width = 320.dp + 64.dp, | ||||||
|  |                 height = 900.dp, | ||||||
|  |             ), | ||||||
|  |             title = "LwaCharacterSheet", | ||||||
|  |         ) { | ||||||
|  |             App() | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | @ -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() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -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) | ||||||
|  | } | ||||||
|  | @ -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) | ||||||
|  | } | ||||||
|  | @ -15,7 +15,6 @@ import androidx.compose.foundation.layout.size | ||||||
| import androidx.compose.foundation.layout.width | import androidx.compose.foundation.layout.width | ||||||
| import androidx.compose.foundation.rememberScrollState | import androidx.compose.foundation.rememberScrollState | ||||||
| import androidx.compose.foundation.verticalScroll | import androidx.compose.foundation.verticalScroll | ||||||
| import androidx.compose.material.Button |  | ||||||
| import androidx.compose.material.Checkbox | import androidx.compose.material.Checkbox | ||||||
| import androidx.compose.material.MaterialTheme | import androidx.compose.material.MaterialTheme | ||||||
| import androidx.compose.material.Text | 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.text.style.TextOverflow | ||||||
| import androidx.compose.ui.unit.Dp | import androidx.compose.ui.unit.Dp | ||||||
| import androidx.compose.ui.unit.dp | import androidx.compose.ui.unit.dp | ||||||
| import com.pixelized.desktop.lwa.composable.DecoratedBox | import com.pixelized.desktop.lwa.composable.decoratedBox.DecoratedBox | ||||||
| import com.pixelized.desktop.lwa.screen.characterSheet.CharacterSheetUio.Node.Type | import com.pixelized.desktop.lwa.screen.characterSheet.CharacterSheetPageUio.Node.Type | ||||||
| 
 | 
 | ||||||
| @Stable | @Stable | ||||||
| data class CharacterSheetUio( | data class CharacterSheetPageUio( | ||||||
|     val name: String, |     val name: String, | ||||||
|     val characteristics: List<Characteristic>, |     val characteristics: List<Characteristic>, | ||||||
|     val subCharacteristics: List<Characteristic>, |     val subCharacteristics: List<Characteristic>, | ||||||
|  | @ -61,7 +60,7 @@ data class CharacterSheetUio( | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     companion object { |     companion object { | ||||||
|         val Koryas = CharacterSheetUio( |         val Koryas = CharacterSheetPageUio( | ||||||
|             name = "Koryas Tissenpa", |             name = "Koryas Tissenpa", | ||||||
|             characteristics = listOf( |             characteristics = listOf( | ||||||
|                 Characteristic(label = "Force", value = "10"), |                 Characteristic(label = "Force", value = "10"), | ||||||
|  | @ -113,13 +112,13 @@ data class CharacterSheetUio( | ||||||
| 
 | 
 | ||||||
| @OptIn(ExperimentalLayoutApi::class) | @OptIn(ExperimentalLayoutApi::class) | ||||||
| @Composable | @Composable | ||||||
| fun CharacterSheet( | fun CharacterSheetPage( | ||||||
|     modifier: Modifier, |     modifier: Modifier, | ||||||
|     scrollState: ScrollState = rememberScrollState(), |     scrollState: ScrollState = rememberScrollState(), | ||||||
|     width: Dp = 320.dp, |     width: Dp = 320.dp, | ||||||
|     characterSheet: CharacterSheetUio = CharacterSheetUio.Koryas, |     characterSheet: CharacterSheetPageUio = CharacterSheetPageUio.Koryas, | ||||||
|     onCharacteristic: (characteristic: CharacterSheetUio.Characteristic) -> Unit, |     onCharacteristic: (characteristic: CharacterSheetPageUio.Characteristic) -> Unit, | ||||||
|     onSkill: (skill: CharacterSheetUio.Node) -> Unit, |     onSkill: (skill: CharacterSheetPageUio.Node) -> Unit, | ||||||
| ) { | ) { | ||||||
|     Column( |     Column( | ||||||
|         modifier = Modifier |         modifier = Modifier | ||||||
|  | @ -247,7 +246,7 @@ fun CharacterSheet( | ||||||
| private fun Stat( | private fun Stat( | ||||||
|     modifier: Modifier = Modifier, |     modifier: Modifier = Modifier, | ||||||
|     paddingValues: PaddingValues = PaddingValues(all = 8.dp), |     paddingValues: PaddingValues = PaddingValues(all = 8.dp), | ||||||
|     characteristic: CharacterSheetUio.Characteristic, |     characteristic: CharacterSheetPageUio.Characteristic, | ||||||
|     onClick: () -> Unit, |     onClick: () -> Unit, | ||||||
| ) { | ) { | ||||||
|     DecoratedBox( |     DecoratedBox( | ||||||
|  | @ -277,7 +276,7 @@ private fun Stat( | ||||||
| private fun Characteristics( | private fun Characteristics( | ||||||
|     modifier: Modifier = Modifier, |     modifier: Modifier = Modifier, | ||||||
|     paddingValues: PaddingValues = PaddingValues(horizontal = 8.dp), |     paddingValues: PaddingValues = PaddingValues(horizontal = 8.dp), | ||||||
|     characteristic: CharacterSheetUio.Characteristic, |     characteristic: CharacterSheetPageUio.Characteristic, | ||||||
| ) { | ) { | ||||||
|     Row( |     Row( | ||||||
|         modifier = Modifier |         modifier = Modifier | ||||||
|  | @ -6,11 +6,11 @@ import androidx.lifecycle.ViewModel | ||||||
| 
 | 
 | ||||||
| class CharacterSheetViewModel : ViewModel() { | class CharacterSheetViewModel : ViewModel() { | ||||||
| 
 | 
 | ||||||
|     private val _sheet = mutableStateOf<CharacterSheetUio?>(null) |     private val _sheet = mutableStateOf<CharacterSheetPageUio?>(null) | ||||||
|     val sheet: State<CharacterSheetUio?> get() = _sheet |     val sheet: State<CharacterSheetPageUio?> get() = _sheet | ||||||
| 
 | 
 | ||||||
|     fun showCharacterSheet() { |     fun showCharacterSheet() { | ||||||
|         _sheet.value = CharacterSheetUio.Koryas |         _sheet.value = CharacterSheetPageUio.Koryas | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fun hideCharacterSheet() { |     fun hideCharacterSheet() { | ||||||
|  |  | ||||||
|  | @ -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, |  | ||||||
|                                     ) |  | ||||||
|                                 } |  | ||||||
|                             } |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -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") | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -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(), | ||||||
|  |                 ), | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -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 | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -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.AnimatedContent | ||||||
| import androidx.compose.animation.fadeIn | import androidx.compose.animation.fadeIn | ||||||
|  | @ -6,8 +6,10 @@ import androidx.compose.animation.fadeOut | ||||||
| import androidx.compose.animation.togetherWith | import androidx.compose.animation.togetherWith | ||||||
| import androidx.compose.foundation.layout.Arrangement | import androidx.compose.foundation.layout.Arrangement | ||||||
| import androidx.compose.foundation.layout.Row | import androidx.compose.foundation.layout.Row | ||||||
|  | import androidx.compose.foundation.layout.padding | ||||||
| import androidx.compose.foundation.layout.width | import androidx.compose.foundation.layout.width | ||||||
| import androidx.compose.foundation.text.KeyboardActions | import androidx.compose.foundation.text.KeyboardActions | ||||||
|  | import androidx.compose.material.MaterialTheme | ||||||
| import androidx.compose.material.Text | import androidx.compose.material.Text | ||||||
| import androidx.compose.material.TextField | import androidx.compose.material.TextField | ||||||
| import androidx.compose.runtime.Composable | import androidx.compose.runtime.Composable | ||||||
|  | @ -19,11 +21,13 @@ import androidx.compose.ui.Alignment | ||||||
| import androidx.compose.ui.Modifier | import androidx.compose.ui.Modifier | ||||||
| import androidx.compose.ui.focus.FocusDirection | import androidx.compose.ui.focus.FocusDirection | ||||||
| import androidx.compose.ui.platform.LocalFocusManager | import androidx.compose.ui.platform.LocalFocusManager | ||||||
|  | import androidx.compose.ui.text.style.TextOverflow | ||||||
| import androidx.compose.ui.unit.dp | import androidx.compose.ui.unit.dp | ||||||
| 
 | 
 | ||||||
| @Stable | @Stable | ||||||
| open class FieldUio( | open class FieldUio( | ||||||
|     val useLabelAsPlaceholder: Boolean, |     val useLabelAsPlaceholder: Boolean, | ||||||
|  |     val isLabelEditable: Boolean, | ||||||
|     val label: State<String>, |     val label: State<String>, | ||||||
|     val onLabelChange: (String) -> Unit, |     val onLabelChange: (String) -> Unit, | ||||||
|     val valuePlaceHolder: State<String>, |     val valuePlaceHolder: State<String>, | ||||||
|  | @ -34,6 +38,7 @@ open class FieldUio( | ||||||
|         @Stable |         @Stable | ||||||
|         fun create( |         fun create( | ||||||
|             useLabelAsPlaceholder: Boolean = false, |             useLabelAsPlaceholder: Boolean = false, | ||||||
|  |             isLabelEditable: Boolean = false, | ||||||
|             label: String = "", |             label: String = "", | ||||||
|             initialValue: String = "", |             initialValue: String = "", | ||||||
|             valuePlaceHolder: () -> String = { "" }, |             valuePlaceHolder: () -> String = { "" }, | ||||||
|  | @ -42,6 +47,7 @@ open class FieldUio( | ||||||
|             val valueState = mutableStateOf(initialValue) |             val valueState = mutableStateOf(initialValue) | ||||||
|             return FieldUio( |             return FieldUio( | ||||||
|                 useLabelAsPlaceholder = useLabelAsPlaceholder, |                 useLabelAsPlaceholder = useLabelAsPlaceholder, | ||||||
|  |                 isLabelEditable = useLabelAsPlaceholder.not() && isLabelEditable, | ||||||
|                 label = labelState, |                 label = labelState, | ||||||
|                 onLabelChange = { labelState.value = it }, |                 onLabelChange = { labelState.value = it }, | ||||||
|                 valuePlaceHolder = derivedStateOf(valuePlaceHolder), |                 valuePlaceHolder = derivedStateOf(valuePlaceHolder), | ||||||
|  | @ -81,14 +87,29 @@ fun Form( | ||||||
|                     horizontalArrangement = Arrangement.spacedBy(space = 8.dp), |                     horizontalArrangement = Arrangement.spacedBy(space = 8.dp), | ||||||
|                     verticalAlignment = Alignment.CenterVertically, |                     verticalAlignment = Alignment.CenterVertically, | ||||||
|                 ) { |                 ) { | ||||||
|                     TextField( |                     AnimatedContent( | ||||||
|                         modifier = Modifier.weight(weight = 1f), |                         modifier = Modifier.weight(weight = 1f), | ||||||
|                         value = field.label.value, |                         targetState = field.isLabelEditable, | ||||||
|                         placeholder = { Text(text = "Nom") }, |                         transitionSpec = { fadeIn() togetherWith fadeOut() }, | ||||||
|                         singleLine = true, |                     ) { editable -> | ||||||
|                         keyboardActions = KeyboardActions { focus.moveFocus(FocusDirection.Next) }, |                         when (editable) { | ||||||
|                         onValueChange = field.onLabelChange, |                             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( |                     TextField( | ||||||
|                         modifier = Modifier.width(width = 80.dp), |                         modifier = Modifier.width(width = 80.dp), | ||||||
|                         value = field.value.value, |                         value = field.value.value, | ||||||
|  | @ -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") | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -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, | ||||||
|  |                     ) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -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 |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -8,7 +8,7 @@ import androidx.compose.runtime.mutableStateOf | ||||||
| import androidx.lifecycle.ViewModel | import androidx.lifecycle.ViewModel | ||||||
| import com.pixelized.desktop.lwa.business.RollUseCase | import com.pixelized.desktop.lwa.business.RollUseCase | ||||||
| import com.pixelized.desktop.lwa.business.SkillStepUseCase | 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.Job | ||||||
| import kotlinx.coroutines.coroutineScope | import kotlinx.coroutines.coroutineScope | ||||||
| import kotlinx.coroutines.delay | import kotlinx.coroutines.delay | ||||||
|  | @ -26,7 +26,7 @@ class RollViewModel : ViewModel() { | ||||||
|     private val _result = mutableStateOf<RollResultUio?>(null) |     private val _result = mutableStateOf<RollResultUio?>(null) | ||||||
|     val result: State<RollResultUio?> get() = _result |     val result: State<RollResultUio?> get() = _result | ||||||
| 
 | 
 | ||||||
|     fun prepareRoll(node: CharacterSheetUio.Node) { |     fun prepareRoll(node: CharacterSheetPageUio.Node) { | ||||||
|         val step = SkillStepUseCase.computeSkillStep( |         val step = SkillStepUseCase.computeSkillStep( | ||||||
|             skill = node.value, |             skill = node.value, | ||||||
|         ) |         ) | ||||||
|  | @ -36,7 +36,7 @@ class RollViewModel : ViewModel() { | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fun prepareRoll(characteristic: CharacterSheetUio.Characteristic) { |     fun prepareRoll(characteristic: CharacterSheetPageUio.Characteristic) { | ||||||
|         val step = SkillStepUseCase.computeSkillStep( |         val step = SkillStepUseCase.computeSkillStep( | ||||||
|             skill = (characteristic.value.toIntOrNull() ?: 0) * 5 |             skill = (characteristic.value.toIntOrNull() ?: 0) * 5 | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  | @ -1,16 +1,23 @@ | ||||||
| [versions] | [versions] | ||||||
| androidx-lifecycle = "2.8.3" |  | ||||||
| compose-multiplatform = "1.7.0" |  | ||||||
| junit = "4.13.2" |  | ||||||
| kotlin = "2.0.21" | kotlin = "2.0.21" | ||||||
| kotlinx-coroutines = "1.9.0" | 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] | [libraries] | ||||||
|  | # Test | ||||||
| kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } | kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } | ||||||
| kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } | kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } | ||||||
| junit = { group = "junit", name = "junit", version.ref = "junit" } | 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-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-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" } | kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" } | ||||||
| 
 | 
 | ||||||
| [plugins] | [plugins] | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue