Update the app with some features skeleton.

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

View file

@ -0,0 +1,12 @@
package com.pixelized.desktop.lwa.business
object RollUseCase {
private val d100 = (1..100)
/**
* (Math.random() * 100 + 1).toInt()
*/
fun rollD100(): Int {
return d100.random()
}
}

View file

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

View file

@ -0,0 +1,95 @@
package com.pixelized.desktop.lwa.business
import kotlin.math.max
import kotlin.math.min
object SkillStepUseCase {
data class SkillStep(
val criticalSuccessRange: IntRange,
val specialSuccessRange: IntRange,
val successRange: IntRange,
val failureRange: IntRange,
val criticalFailureRange: IntRange,
) {
constructor(
criticalSuccess: Pair<Int, Int>?,
specialSuccess: Pair<Int, Int>,
success: Pair<Int, Int>,
failure: Pair<Int, Int>?,
criticalFailure: Pair<Int, Int>,
) : this(
criticalSuccessRange = criticalSuccess
?.let { IntRange(it.first, it.second) }
?: IntRange(-1, -1),
specialSuccessRange = specialSuccess
.let { IntRange(it.first, it.second) },
successRange = success
.let { IntRange(it.first, it.second) },
failureRange = failure
?.let { IntRange(it.first, it.second) } ?: IntRange(-1, -1),
criticalFailureRange = criticalFailure
.let { IntRange(it.first, it.second) },
)
}
/**
* Helper method to compute the range in which a roll is a either critical, special, success or failure.
* TODO : test.
*/
fun computeSkillStep(skill: Int): SkillStep {
val criticalSuccess: Pair<Int, Int>? = when (skill) {
in (0..5) -> null
in (10..25) -> 1 to 1
in (30..45) -> 1 to 2
in (50..65) -> 1 to 3
in (70..85) -> 1 to 4
in (90..100) -> 1 to 5
else -> 1 to skill * 5 / 100
}
val specialSuccess: Pair<Int, Int> = when (skill) {
0, 5 -> 1 to 1
10 -> 2 to 2
15 -> ((criticalSuccess?.second ?: 0) + 1) to 3
20 -> ((criticalSuccess?.second ?: 0) + 1) to 4
25 -> ((criticalSuccess?.second ?: 0) + 1) to 5
30 -> ((criticalSuccess?.second ?: 0) + 1) to 6
35 -> ((criticalSuccess?.second ?: 0) + 1) to 7
40 -> ((criticalSuccess?.second ?: 0) + 1) to 8
45 -> ((criticalSuccess?.second ?: 0) + 1) to 9
50 -> ((criticalSuccess?.second ?: 0) + 1) to 10
55 -> ((criticalSuccess?.second ?: 0) + 1) to 11
60 -> ((criticalSuccess?.second ?: 0) + 1) to 12
65 -> ((criticalSuccess?.second ?: 0) + 1) to 13
70 -> ((criticalSuccess?.second ?: 0) + 1) to 14
75 -> ((criticalSuccess?.second ?: 0) + 1) to 15
80 -> ((criticalSuccess?.second ?: 0) + 1) to 16
85 -> ((criticalSuccess?.second ?: 0) + 1) to 17
90 -> ((criticalSuccess?.second ?: 0) + 1) to 18
95 -> ((criticalSuccess?.second ?: 0) + 1) to 19
100 -> ((criticalSuccess?.second ?: 0) + 1) to 20
else -> ((criticalSuccess?.second ?: 0) + 1) to skill * 20 / 100
}
val success: Pair<Int, Int> = (specialSuccess.second + 1) to max(5, min(99, skill))
val criticalFailure: Pair<Int, Int> = when (skill) {
0, 5, 10 -> 96 to 100
15, 20, 25, 30 -> 97 to 100
35, 40, 45, 50 -> 98 to 100
55, 60, 65, 70 -> 99 to 100
else -> 100 to 100
}
val failure: Pair<Int, Int>? = if (skill >= 100) {
null
} else {
success.second + 1 to criticalFailure.first - 1
}
return SkillStep(
criticalSuccess = criticalSuccess,
specialSuccess = specialSuccess,
success = success,
failure = failure,
criticalFailure = criticalFailure,
)
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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