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
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue