Add roll history page

This commit is contained in:
Thomas Andres Gomez 2024-11-10 17:26:58 +01:00
parent f92922c228
commit ca456b55d9
20 changed files with 488 additions and 133 deletions

View file

@ -1,7 +1,9 @@
plugins { plugins {
// this is necessary to avoid the plugins to be loaded multiple times // this is necessary to avoid the plugins to be loaded multiple times
// in each subproject's classloader // in each subproject's classloader
alias(libs.plugins.kotlinMultiplatform) apply false
alias(libs.plugins.kotlinSerialization) apply false
// alias(libs.plugins.kotlinKtor) apply false
alias(libs.plugins.composeMultiplatform) apply false alias(libs.plugins.composeMultiplatform) apply false
alias(libs.plugins.composeCompiler) apply false alias(libs.plugins.composeCompiler) apply false
alias(libs.plugins.kotlinMultiplatform) apply false
} }

View file

@ -3,10 +3,9 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat
plugins { plugins {
alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.kotlinSerialization) alias(libs.plugins.kotlinSerialization)
// alias(libs.plugins.kotlinKtor)
alias(libs.plugins.composeMultiplatform) alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler) alias(libs.plugins.composeCompiler)
// kotlin("jvm") version "1.9.20"
// alias(libs.plugins.kotlinKtor)
} }
kotlin { kotlin {

View file

@ -1,14 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<resources> <resources>
<string name="main_page__create_action">Créer une feuille de personnage</string> <string name="main_page__create_action">Créer une feuille de personnage</string>
<string name="main_page__network_action">Configuration réseau</string> <string name="main_page__network_action">Configuration de la table</string>
<string name="main_page__roll_history_action">Consulter l'historique des lancés</string> <string name="main_page__roll_history_action">Consulter l'historique des lancers</string>
<string name="roll_page__roll__label">Jet de :</string>
<string name="roll_page__roll__success_label">Réussite si lancer inférieur ou égale à : %1$s</string>
<string name="roll_page__critical_success">Réussite critique</string> <string name="roll_page__critical_success">Réussite critique</string>
<string name="roll_page__special_success">Réussite spéciale</string> <string name="roll_page__special_success">Réussite spéciale</string>
<string name="roll_page__success">Réussite</string> <string name="roll_page__success">Réussite</string>
<string name="roll_page__failure">Échec</string> <string name="roll_page__failure">Échec</string>
<string name="roll_page__critical_failure">Échec critique</string> <string name="roll_page__critical_failure">Échec critique</string>
<string name="roll_page__dc__label">Dégré de difficulté :</string>
<string name="roll_page__dc_easy__label">Facile</string>
<string name="roll_page__dc_normal__label">Normal</string>
<string name="roll_page__dc_hard__label">Difficile</string>
<string name="roll_page__dc_impossible__label">Impossible</string>
<string name="character_sheet_edit__title">Création de personnage</string> <string name="character_sheet_edit__title">Création de personnage</string>
<string name="character_sheet_edit__name_placeholder">Nom</string> <string name="character_sheet_edit__name_placeholder">Nom</string>
@ -68,7 +75,7 @@
<string name="character_sheet__occupations_title">Occupations</string> <string name="character_sheet__occupations_title">Occupations</string>
<string name="character_sheet__magics__title">Compétences magiques</string> <string name="character_sheet__magics__title">Compétences magiques</string>
<string name="network__title">Configuration réseau</string> <string name="network__title">Configuration de la table</string>
<string name="network__player_name__label">Nom du joueur</string> <string name="network__player_name__label">Nom du joueur</string>
<string name="network__host__label">host</string> <string name="network__host__label">host</string>
<string name="network__port__label">port</string> <string name="network__port__label">port</string>
@ -83,5 +90,7 @@
<string name="network__socket__type_client">Client</string> <string name="network__socket__type_client">Client</string>
<string name="network__socket__type_none">Aucun</string> <string name="network__socket__type_none">Aucun</string>
<string name="roll_history__title">Historique des lancés</string> <string name="roll_history__title">Historique des lancers</string>
<string name="roll_history__item__throw">lance</string>
<string name="roll_history__item__difficulty">Difficulté</string>
</resources> </resources>

View file

@ -168,7 +168,10 @@ fun HandleCharacterSheetCreation(
title = stringResource(Res.string.character_sheet_edit__title), title = stringResource(Res.string.character_sheet_edit__title),
) { ) {
CharacterSheetMainNavHost( CharacterSheetMainNavHost(
startDestination = CharacterSheetEditDestination.navigationRoute(id = null) startDestination = CharacterSheetEditDestination.navigationRoute(
id = null,
enableBack = false,
)
) )
} }
} }

View file

@ -3,6 +3,7 @@ package com.pixelized.desktop.lwa.navigation.destination
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.navigation.NavGraphBuilder import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.navArgument import androidx.navigation.navArgument
import com.pixelized.desktop.lwa.screen.characterSheet.edit.CharacterSheetEditPage import com.pixelized.desktop.lwa.screen.characterSheet.edit.CharacterSheetEditPage
@ -11,22 +12,33 @@ import com.pixelized.desktop.lwa.utils.extention.ARG
object CharacterSheetEditDestination { object CharacterSheetEditDestination {
private const val ROUTE = "character.sheet.edit" private const val ROUTE = "character.sheet.edit"
private const val CHARACTER_ID = "id" private const val CHARACTER_ID = "id"
private const val ENABLE_BACK = "enable_back"
fun baseRoute() = "$ROUTE?${CHARACTER_ID.ARG}" fun baseRoute() = "$ROUTE?${CHARACTER_ID.ARG}&${ENABLE_BACK.ARG}"
fun navigationRoute(id: String?) = "$ROUTE?$CHARACTER_ID=$id" fun navigationRoute(id: String?, enableBack: Boolean) = ROUTE +
"?$CHARACTER_ID=$id" +
"&$ENABLE_BACK=$enableBack"
fun arguments() = listOf( fun arguments() = listOf(
navArgument(CHARACTER_ID) { navArgument(CHARACTER_ID) {
nullable = true nullable = true
type = NavType.StringType
},
navArgument(ENABLE_BACK) {
nullable = false
type = NavType.BoolType
} }
) )
data class Argument( data class Argument(
val id: String?, val id: String?,
val enableBack: Boolean,
) { ) {
constructor(savedStateHandle: SavedStateHandle) : this( constructor(savedStateHandle: SavedStateHandle) : this(
id = savedStateHandle.get<String>(CHARACTER_ID) id = savedStateHandle.get<String>(CHARACTER_ID),
enableBack = savedStateHandle.get<Boolean>(ENABLE_BACK)
?: error("Missing enableBack argument"),
) )
} }
} }
@ -42,7 +54,8 @@ fun NavGraphBuilder.composableCharacterSheetEditPage() {
fun NavHostController.navigateToCharacterSheetEdit( fun NavHostController.navigateToCharacterSheetEdit(
id: String? = null, id: String? = null,
enableBack: Boolean = true,
) { ) {
val route = CharacterSheetEditDestination.navigationRoute(id = id) val route = CharacterSheetEditDestination.navigationRoute(id = id, enableBack = enableBack)
navigate(route = route) navigate(route = route)
} }

View file

@ -27,6 +27,9 @@ typealias Server = EmbeddedServer<NettyApplicationEngine, NettyApplicationEngine
typealias Client = HttpClient typealias Client = HttpClient
object NetworkRepository { object NetworkRepository {
const val DEFAULT_PORT = 16030
const val DEFAULT_HOST = "pixelized.freeboxos.fr"
private val scope = CoroutineScope(Dispatchers.IO) private val scope = CoroutineScope(Dispatchers.IO)
private var networkJob: Job? = null private var networkJob: Job? = null
private var server: Server? = null private var server: Server? = null

View file

@ -15,7 +15,7 @@ import kotlin.time.Duration.Companion.seconds
// https://ktor.io/docs/server-websockets.html#handle-multiple-session // https://ktor.io/docs/server-websockets.html#handle-multiple-session
fun server( fun server(
port: Int = 8080, port: Int,
handler: suspend DefaultWebSocketServerSession.() -> Unit handler: suspend DefaultWebSocketServerSession.() -> Unit
): EmbeddedServer<NettyApplicationEngine, NettyApplicationEngine.Configuration> { ): EmbeddedServer<NettyApplicationEngine, NettyApplicationEngine.Configuration> {
return embeddedServer( return embeddedServer(

View file

@ -4,6 +4,9 @@ import kotlinx.serialization.Serializable
@Serializable @Serializable
data class RollMessage( data class RollMessage(
val label: String, val skillLabel: String,
val roll: Int, val resultLabel: String?,
): MessageContent val rollDifficulty: String?,
val rollValue: Int,
val rollSuccessLimit: Int?,
) : MessageContent

View file

@ -31,11 +31,20 @@ object RollHistoryRepository {
} }
suspend fun share( suspend fun share(
label: String, skillLabel: String,
roll: Int, rollDifficulty: String?,
rollValue: Int,
resultLabel: String?,
rollSuccessLimit: Int?,
) { ) {
network.share( network.share(
content = RollMessage(label = label, roll = roll) content = RollMessage(
skillLabel = skillLabel,
rollDifficulty = rollDifficulty,
rollValue = rollValue,
resultLabel = resultLabel,
rollSuccessLimit = rollSuccessLimit,
)
) )
} }
} }

View file

@ -113,11 +113,11 @@ fun CharacterSheetPage(
CharacterSheetPageContent( CharacterSheetPageContent(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
characterSheet = sheet, characterSheet = sheet,
onBack = {
screen.popBackStack()
},
onEdit = { onEdit = {
screen.navigateToCharacterSheetEdit(id = sheet.id) screen.navigateToCharacterSheetEdit(
id = sheet.id,
enableBack = true,
)
}, },
onDelete = { onDelete = {
scope.launch { scope.launch {
@ -154,7 +154,6 @@ fun CharacterSheetPageContent(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
scrollState: ScrollState = rememberScrollState(), scrollState: ScrollState = rememberScrollState(),
characterSheet: CharacterSheetPageUio, characterSheet: CharacterSheetPageUio,
onBack: () -> Unit,
onEdit: () -> Unit, onEdit: () -> Unit,
onDelete: () -> Unit, onDelete: () -> Unit,
onCharacteristic: (characteristic: CharacterSheetPageUio.Characteristic) -> Unit, onCharacteristic: (characteristic: CharacterSheetPageUio.Characteristic) -> Unit,
@ -178,6 +177,7 @@ fun CharacterSheetPageContent(
) { ) {
Icon( Icon(
imageVector = Icons.Default.Edit, imageVector = Icons.Default.Edit,
tint = MaterialTheme.colors.primary,
contentDescription = null, contentDescription = null,
) )
} }
@ -186,6 +186,7 @@ fun CharacterSheetPageContent(
) { ) {
Icon( Icon(
imageVector = Icons.Default.Delete, imageVector = Icons.Default.Delete,
tint = MaterialTheme.colors.primary,
contentDescription = null, contentDescription = null,
) )
} }
@ -346,9 +347,10 @@ private fun Stat(
) )
Text( Text(
modifier = Modifier.align(alignment = Alignment.Center), modifier = Modifier.align(alignment = Alignment.Center),
style = MaterialTheme.typography.h4, style = MaterialTheme.typography.h3,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
maxLines = 1, maxLines = 1,
color = MaterialTheme.colors.primary,
text = characteristic.value text = characteristic.value
) )
} }
@ -402,6 +404,7 @@ private fun Roll(
) )
Icon( Icon(
painter = painterResource(Res.drawable.ic_d20_32dp), painter = painterResource(Res.drawable.ic_d20_32dp),
tint = MaterialTheme.colors.primary,
contentDescription = null, contentDescription = null,
) )
} }
@ -433,6 +436,7 @@ private fun Skill(
Text( Text(
style = MaterialTheme.typography.body1, style = MaterialTheme.typography.body1,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = MaterialTheme.colors.primary,
text = "$value", text = "$value",
) )
Checkbox(modifier = Modifier.size(size = 32.dp), checked = false, onCheckedChange = { }) Checkbox(modifier = Modifier.size(size = 32.dp), checked = false, onCheckedChange = { })

View file

@ -22,6 +22,7 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -85,7 +86,13 @@ fun CharacterSheetEditPage(
) { ) {
CharacterSheetEdit( CharacterSheetEdit(
form = viewModel.characterSheet.value, form = viewModel.characterSheet.value,
onBack = { screen.popBackStack() }, onBack = remember {
if (viewModel.enableBack) {
{ screen.popBackStack() }
} else {
null
}
},
onNewSkill = viewModel::onSkill, onNewSkill = viewModel::onSkill,
onNewCategory = viewModel::onNewRoll, onNewCategory = viewModel::onNewRoll,
onSave = { onSave = {
@ -103,7 +110,7 @@ fun CharacterSheetEditPage(
@Composable @Composable
fun CharacterSheetEdit( fun CharacterSheetEdit(
form: CharacterSheetEditPageUio, form: CharacterSheetEditPageUio,
onBack: () -> Unit, onBack: (() -> Unit)?,
onNewSkill: (CharacterSheetEditPageUio.SkillGroup) -> Unit, onNewSkill: (CharacterSheetEditPageUio.SkillGroup) -> Unit,
onNewCategory: () -> Unit, onNewCategory: () -> Unit,
onSave: () -> Unit, onSave: () -> Unit,
@ -118,14 +125,16 @@ fun CharacterSheetEdit(
text = stringResource(Res.string.character_sheet_edit__title), text = stringResource(Res.string.character_sheet_edit__title),
) )
}, },
navigationIcon = { navigationIcon = onBack?.let { action: () -> Unit ->
IconButton( {
onClick = onBack, IconButton(
) { onClick = action,
Icon( ) {
imageVector = Icons.AutoMirrored.Filled.ArrowBack, Icon(
contentDescription = null, imageVector = Icons.AutoMirrored.Filled.ArrowBack,
) contentDescription = null,
)
}
} }
} }
) )

View file

@ -18,6 +18,8 @@ class CharacterSheetEditViewModel(
private val repository = CharacterSheetRepository private val repository = CharacterSheetRepository
private val factory = CharacterSheetEditFactory() private val factory = CharacterSheetEditFactory()
val enableBack = argument.enableBack
private val _characterSheet = mutableStateOf( private val _characterSheet = mutableStateOf(
repository.characterSheetFlow(id = argument.id).value.let { repository.characterSheetFlow(id = argument.id).value.let {
runBlocking { factory.convertToUio(it) } runBlocking { factory.convertToUio(it) }

View file

@ -14,8 +14,9 @@ class NetworkViewModel : ViewModel() {
private val repository = NetworkRepository private val repository = NetworkRepository
private val factory = NetworkFactory() private val factory = NetworkFactory()
private val host = mutableStateOf("localhost") private val host = mutableStateOf(NetworkRepository.DEFAULT_HOST)
private val port = mutableStateOf(27030) private val port = mutableStateOf(NetworkRepository.DEFAULT_PORT)
val network: State<NetworkPageUio> val network: State<NetworkPageUio>
@Composable @Composable
@Stable @Stable

View file

@ -1,6 +1,7 @@
package com.pixelized.desktop.lwa.screen.roll package com.pixelized.desktop.lwa.screen.roll
import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideInVertically
@ -11,15 +12,22 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
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.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ExposedDropdownMenuBox
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -34,13 +42,23 @@ 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.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.pixelized.desktop.lwa.screen.roll.DifficultyUio.Difficulty
import com.pixelized.desktop.lwa.utils.DisableInteractionSource
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.ic_d20_32dp import lwacharactersheet.composeapp.generated.resources.ic_d20_32dp
import lwacharactersheet.composeapp.generated.resources.roll_page__dc__label
import lwacharactersheet.composeapp.generated.resources.roll_page__dc_easy__label
import lwacharactersheet.composeapp.generated.resources.roll_page__dc_hard__label
import lwacharactersheet.composeapp.generated.resources.roll_page__dc_impossible__label
import lwacharactersheet.composeapp.generated.resources.roll_page__dc_normal__label
import lwacharactersheet.composeapp.generated.resources.roll_page__roll__label
import lwacharactersheet.composeapp.generated.resources.roll_page__roll__success_label
import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource
@Stable @Stable
data class RollUio( data class RollTitleUio(
val label: String, val label: String,
val value: Int?, val value: Int?,
) )
@ -51,6 +69,16 @@ data class RollResultUio(
val value: Int, val value: Int,
) )
@Stable
data class DifficultyUio(
val open: Boolean,
val difficulty: Difficulty,
) {
enum class Difficulty {
EASY, NORMAL, HARD, IMPOSSIBLE
}
}
@Composable @Composable
fun RollPage( fun RollPage(
viewModel: RollViewModel = viewModel { RollViewModel() }, viewModel: RollViewModel = viewModel { RollViewModel() },
@ -72,24 +100,24 @@ fun RollPage(
style = MaterialTheme.typography.caption, style = MaterialTheme.typography.caption,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
text = "Jet de :", text = stringResource(Res.string.roll_page__roll__label),
) )
Text( Text(
style = MaterialTheme.typography.h5, style = MaterialTheme.typography.h5,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
text = viewModel.roll.value.label, text = viewModel.rollTitle.value.label,
) )
viewModel.roll.value.value?.let { viewModel.rollTitle.value.value?.let {
Text( Text(
style = MaterialTheme.typography.caption, style = MaterialTheme.typography.caption,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
text = "Réussite en dessous de : ${it}", text = stringResource(Res.string.roll_page__roll__success_label, it),
) )
} }
Column( Column(
modifier = Modifier.fillMaxSize(), modifier = Modifier.weight(weight = 1f),
verticalArrangement = Arrangement.spacedBy( verticalArrangement = Arrangement.spacedBy(
space = 24.dp, space = 24.dp,
alignment = Alignment.CenterVertically, alignment = Alignment.CenterVertically,
@ -114,7 +142,7 @@ fun RollPage(
contentDescription = null, contentDescription = null,
) )
AnimatedContent( AnimatedContent(
targetState = viewModel.result.value?.value?.toString() ?: "", targetState = viewModel.rollResult.value?.value?.toString() ?: "",
transitionSpec = { transitionSpec = {
val enter = fadeIn() + slideInVertically { 32 } val enter = fadeIn() + slideInVertically { 32 }
val exit = fadeOut() + slideOutVertically { -32 } val exit = fadeOut() + slideOutVertically { -32 }
@ -137,7 +165,7 @@ fun RollPage(
} }
} }
AnimatedContent( AnimatedContent(
targetState = viewModel.result.value?.label ?: "", targetState = viewModel.rollResult.value?.label ?: "",
transitionSpec = { fadeIn() togetherWith fadeOut() }, transitionSpec = { fadeIn() togetherWith fadeOut() },
) { value -> ) { value ->
Text( Text(
@ -148,5 +176,118 @@ fun RollPage(
) )
} }
} }
viewModel.rollDifficulty.value?.let {
Box(
modifier = Modifier.clickable(
interactionSource = remember { DisableInteractionSource() },
indication = null,
onClick = {},
)
) {
Difficulty(
difficulty = it,
onToggle = { viewModel.toggleDifficulty() },
onDifficulty = { viewModel.onDifficulty(it) }
)
}
}
} }
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun Difficulty(
modifier: Modifier = Modifier,
difficulty: DifficultyUio,
onToggle: () -> Unit,
onDifficulty: (Difficulty) -> Unit
) {
ExposedDropdownMenuBox(
modifier = modifier,
expanded = difficulty.open,
onExpandedChange = { onToggle() },
) {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Row(
modifier = Modifier.padding(
horizontal = 16.dp,
vertical = 8.dp,
),
horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
) {
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.body1,
text = stringResource(Res.string.roll_page__dc__label)
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.body1,
color = MaterialTheme.colors.primary,
text = when (difficulty.difficulty) {
Difficulty.EASY -> stringResource(Res.string.roll_page__dc_easy__label)
Difficulty.NORMAL -> stringResource(Res.string.roll_page__dc_normal__label)
Difficulty.HARD -> stringResource(Res.string.roll_page__dc_hard__label)
Difficulty.IMPOSSIBLE -> stringResource(Res.string.roll_page__dc_impossible__label)
}
)
}
val rotation = animateFloatAsState(
targetValue = if (difficulty.open) -180f else 0f,
)
Icon(
modifier = Modifier.graphicsLayer {
rotationZ = rotation.value
},
imageVector = Icons.Default.ArrowDropDown,
contentDescription = null
)
}
ExposedDropdownMenu(
expanded = difficulty.open,
onDismissRequest = { onToggle() }
) {
DifficultyDropDownItem(
label = stringResource(Res.string.roll_page__dc_easy__label),
onClick = { onDifficulty(Difficulty.EASY) },
)
DifficultyDropDownItem(
label = stringResource(Res.string.roll_page__dc_normal__label),
onClick = { onDifficulty(Difficulty.NORMAL) },
)
DifficultyDropDownItem(
label = stringResource(Res.string.roll_page__dc_hard__label),
onClick = { onDifficulty(Difficulty.HARD) },
)
DifficultyDropDownItem(
label = stringResource(Res.string.roll_page__dc_impossible__label),
onClick = { onDifficulty(Difficulty.IMPOSSIBLE) },
)
}
}
}
@Composable
private fun DifficultyDropDownItem(
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(horizontal = 16.dp),
label: String,
onClick: () -> Unit,
) {
DropdownMenuItem(
modifier = modifier,
onClick = onClick,
enabled = true,
contentPadding = contentPadding,
interactionSource = remember { MutableInteractionSource() },
content = {
Text(
color = MaterialTheme.colors.primary,
text = label,
)
},
)
} }

View file

@ -12,6 +12,7 @@ import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheet
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.desktop.lwa.repository.roll.RollHistoryRepository import com.pixelized.desktop.lwa.repository.roll.RollHistoryRepository
import com.pixelized.desktop.lwa.screen.characterSheet.detail.CharacterSheetPageUio import com.pixelized.desktop.lwa.screen.characterSheet.detail.CharacterSheetPageUio
import com.pixelized.desktop.lwa.screen.roll.DifficultyUio.Difficulty
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -20,6 +21,10 @@ import kotlinx.coroutines.runBlocking
import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.roll_page__critical_failure import lwacharactersheet.composeapp.generated.resources.roll_page__critical_failure
import lwacharactersheet.composeapp.generated.resources.roll_page__critical_success import lwacharactersheet.composeapp.generated.resources.roll_page__critical_success
import lwacharactersheet.composeapp.generated.resources.roll_page__dc_easy__label
import lwacharactersheet.composeapp.generated.resources.roll_page__dc_hard__label
import lwacharactersheet.composeapp.generated.resources.roll_page__dc_impossible__label
import lwacharactersheet.composeapp.generated.resources.roll_page__dc_normal__label
import lwacharactersheet.composeapp.generated.resources.roll_page__failure import lwacharactersheet.composeapp.generated.resources.roll_page__failure
import lwacharactersheet.composeapp.generated.resources.roll_page__special_success import lwacharactersheet.composeapp.generated.resources.roll_page__special_success
import lwacharactersheet.composeapp.generated.resources.roll_page__success import lwacharactersheet.composeapp.generated.resources.roll_page__success
@ -28,30 +33,32 @@ import org.jetbrains.compose.resources.getString
class RollViewModel : ViewModel() { class RollViewModel : ViewModel() {
private val repository = RollHistoryRepository private val repository = RollHistoryRepository
private val _roll = mutableStateOf(RollUio(label = "", value = 0)) private lateinit var sheet: CharacterSheet
val roll: State<RollUio> get() = _roll private lateinit var rollAction: String
private var rollSuccessValue: Int? = null
private var rollJob: Job? = null private var rollJob: Job? = null
private var rollStep: SkillStepUseCase.SkillStep? = null
private lateinit var rollAction: String private val _rollTitle = mutableStateOf(RollTitleUio(label = "", value = 0))
private lateinit var sheet: CharacterSheet val rollTitle: State<RollTitleUio> get() = _rollTitle
private val _rollResult = mutableStateOf<RollResultUio?>(null)
val rollResult: State<RollResultUio?> get() = _rollResult
val rollRotation = Animatable(0f) val rollRotation = Animatable(0f)
private val _result = mutableStateOf<RollResultUio?>(null) private val _rollDifficulty = mutableStateOf<DifficultyUio?>(null)
val result: State<RollResultUio?> get() = _result val rollDifficulty: State<DifficultyUio?> get() = _rollDifficulty
fun prepareRoll( fun prepareRoll(
sheet: CharacterSheetPageUio, sheet: CharacterSheetPageUio,
characteristic: CharacterSheetPageUio.Characteristic, characteristic: CharacterSheetPageUio.Characteristic,
) { ) {
val step = SkillStepUseCase.computeSkillStep(
skill = (characteristic.value.toIntOrNull() ?: 0) * 5
)
prepareRoll( prepareRoll(
sheet = sheet,
label = characteristic.label, label = characteristic.label,
rollAction = "1d100", rollAction = "1d100",
sheet = sheet, rollSuccessValue = (characteristic.value.toIntOrNull() ?: 0) * 5,
rollStep = step,
) )
} }
@ -59,14 +66,11 @@ class RollViewModel : ViewModel() {
sheet: CharacterSheetPageUio, sheet: CharacterSheetPageUio,
node: CharacterSheetPageUio.Node, node: CharacterSheetPageUio.Node,
) { ) {
val step = SkillStepUseCase.computeSkillStep(
skill = node.value,
)
prepareRoll( prepareRoll(
sheet = sheet,
label = node.label, label = node.label,
rollAction = "1d100", rollAction = "1d100",
sheet = sheet, rollSuccessValue = node.value,
rollStep = step,
) )
} }
@ -75,34 +79,45 @@ class RollViewModel : ViewModel() {
roll: CharacterSheetPageUio.Roll, roll: CharacterSheetPageUio.Roll,
) { ) {
prepareRoll( prepareRoll(
sheet = sheet,
label = roll.label, label = roll.label,
rollAction = roll.value, rollAction = roll.value,
sheet = sheet, rollSuccessValue = null,
rollStep = null,
) )
} }
private fun prepareRoll( private fun prepareRoll(
sheet: CharacterSheetPageUio,
label: String, label: String,
rollAction: String, rollAction: String,
sheet: CharacterSheetPageUio, rollSuccessValue: Int?,
rollStep: SkillStepUseCase.SkillStep?,
) { ) {
runBlocking { rollRotation.snapTo(0f) } runBlocking { rollRotation.snapTo(0f) }
this.rollStep = rollStep
this.rollAction = rollAction
this.sheet = CharacterSheetRepository.characterSheetFlow(id = sheet.id).value!!
_result.value = null this.sheet = CharacterSheetRepository.characterSheetFlow(id = sheet.id).value!!
_roll.value = RollUio( this.rollAction = rollAction
this.rollSuccessValue = rollSuccessValue
val rollStep = rollSuccessValue?.let {
SkillStepUseCase.computeSkillStep(skill = it)
}
_rollResult.value = null
_rollTitle.value = RollTitleUio(
label = label, label = label,
value = rollStep?.successRange?.last value = rollStep?.successRange?.last
) )
_rollDifficulty.value = rollSuccessValue?.let {
DifficultyUio(
open = false,
Difficulty.NORMAL,
)
}
} }
suspend fun roll() { suspend fun roll() {
coroutineScope { coroutineScope {
_result.value = null _rollResult.value = null
rollJob?.cancel() rollJob?.cancel()
rollJob = launch { rollJob = launch {
@ -118,31 +133,81 @@ class RollViewModel : ViewModel() {
launch { launch {
delay(500) delay(500)
val rollStep = rollSuccessValue?.let {
SkillStepUseCase.computeSkillStep(
skill = when (_rollDifficulty.value?.difficulty) {
Difficulty.EASY -> it * 2
Difficulty.NORMAL -> it
Difficulty.HARD -> it / 2
Difficulty.IMPOSSIBLE -> it / 4
else -> it
}
)
}
val roll = if (rollAction == "1d100") { val roll = if (rollAction == "1d100") {
RollUseCase.rollD100() RollUseCase.rollD100()
} else { } else {
RollUseCase.roll(characterSheet = sheet, roll = rollAction) RollUseCase.roll(characterSheet = sheet, roll = rollAction)
} }
val success = rollStep?.let {
when (roll) {
in it.criticalSuccessRange -> getString(resource = Res.string.roll_page__critical_success)
in it.specialSuccessRange -> getString(resource = Res.string.roll_page__special_success)
in it.successRange -> getString(resource = Res.string.roll_page__success)
in it.failureRange -> getString(resource = Res.string.roll_page__failure)
in it.criticalFailureRange -> getString(resource = Res.string.roll_page__critical_failure)
else -> ""
}
}
_result.value = RollResultUio( _rollResult.value = RollResultUio(
label = rollStep?.let { rollStep -> label = success ?: "",
when (roll) {
in rollStep.criticalSuccessRange -> getString(resource = Res.string.roll_page__critical_success)
in rollStep.specialSuccessRange -> getString(resource = Res.string.roll_page__special_success)
in rollStep.successRange -> getString(resource = Res.string.roll_page__success)
in rollStep.failureRange -> getString(resource = Res.string.roll_page__failure)
in rollStep.criticalFailureRange -> getString(resource = Res.string.roll_page__critical_failure)
else -> ""
}
} ?: "",
value = roll, value = roll,
) )
launch { launch {
repository.share(label = _roll.value.label, roll = roll) repository.share(
skillLabel = _rollTitle.value.label,
rollDifficulty = when (_rollDifficulty.value?.difficulty) {
Difficulty.EASY -> getString(Res.string.roll_page__dc_easy__label)
Difficulty.NORMAL -> getString(Res.string.roll_page__dc_normal__label)
Difficulty.HARD -> getString(Res.string.roll_page__dc_hard__label)
Difficulty.IMPOSSIBLE -> getString(Res.string.roll_page__dc_impossible__label)
else -> null
},
rollValue = roll,
rollSuccessLimit = rollStep?.successRange?.last,
resultLabel = success,
)
} }
} }
} }
} }
} }
fun toggleDifficulty() {
_rollDifficulty.value = _rollDifficulty.value?.copy(
open = _rollDifficulty.value?.open?.not() ?: false
)
}
fun onDifficulty(difficulty: Difficulty) {
_rollDifficulty.value = DifficultyUio(
open = false,
difficulty = difficulty,
)
val rollStep = rollSuccessValue?.let {
SkillStepUseCase.computeSkillStep(
skill = when (_rollDifficulty.value?.difficulty) {
Difficulty.EASY -> it * 2
Difficulty.NORMAL -> it
Difficulty.HARD -> it / 2
Difficulty.IMPOSSIBLE -> it / 4
else -> it
}
)
}
_rollTitle.value = _rollTitle.value.copy(
value = rollStep?.successRange?.last
)
}
} }

View file

@ -0,0 +1,111 @@
package com.pixelized.desktop.lwa.screen.rollhistory
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.roll_history__item__difficulty
import lwacharactersheet.composeapp.generated.resources.roll_history__item__throw
import org.jetbrains.compose.resources.stringResource
@Stable
data class RollHistoryItemUio(
val from: String,
val skillLabel: String,
val rollDifficulty: String?,
val rollValue: Int,
val rollSuccessLimit: Int?,
val resultLabel: String?,
)
@Composable
fun RollHistoryItem(
modifier: Modifier = Modifier,
roll: RollHistoryItemUio,
) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Column(
modifier = Modifier.weight(1f)
) {
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.body1,
fontWeight = FontWeight.Bold,
text = roll.from,
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.caption,
text = stringResource(Res.string.roll_history__item__throw),
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.body1,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = roll.skillLabel,
)
}
Row(
horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
) {
roll.rollDifficulty?.let {
Text(
style = MaterialTheme.typography.caption,
text = stringResource(Res.string.roll_history__item__difficulty),
)
Text(
style = MaterialTheme.typography.caption,
text = it,
)
Text(
style = MaterialTheme.typography.caption,
text = "-",
)
}
roll.resultLabel?.let {
Text(
style = MaterialTheme.typography.caption,
fontWeight = FontWeight.Bold,
text = it,
)
}
}
}
Row(
horizontalArrangement = Arrangement.spacedBy(2.dp),
) {
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.h6,
fontWeight = FontWeight.Bold,
text = "${roll.rollValue}",
)
roll.rollSuccessLimit?.let {
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.caption,
fontWeight = FontWeight.Light,
text = "/ $it",
)
}
}
}
}

View file

@ -2,14 +2,13 @@ package com.pixelized.desktop.lwa.screen.rollhistory
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
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.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.IconButton import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold import androidx.compose.material.Scaffold
import androidx.compose.material.Surface import androidx.compose.material.Surface
import androidx.compose.material.Text import androidx.compose.material.Text
@ -17,10 +16,8 @@ import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
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.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
@ -29,12 +26,6 @@ import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.roll_history__title import lwacharactersheet.composeapp.generated.resources.roll_history__title
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
@Stable
data class RollItemUio(
val from: String,
val label: String,
val roll: Int,
)
@Composable @Composable
fun RollHistoryPage( fun RollHistoryPage(
@ -58,7 +49,7 @@ fun RollHistoryPage(
@Composable @Composable
private fun RollHistoryContent( private fun RollHistoryContent(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
rolls: State<List<RollItemUio>>, rolls: State<List<RollHistoryItemUio>>,
onBack: () -> Unit, onBack: () -> Unit,
) { ) {
Scaffold( Scaffold(
@ -92,10 +83,11 @@ private fun RollHistoryContent(
state = state, state = state,
reverseLayout = true, reverseLayout = true,
contentPadding = PaddingValues(all = 24.dp), contentPadding = PaddingValues(all = 24.dp),
verticalArrangement = Arrangement.spacedBy(space = 8.dp) verticalArrangement = Arrangement.spacedBy(space = 16.dp)
) { ) {
items(items = rolls.value) { items(items = rolls.value) {
RollItem( RollHistoryItem(
modifier = Modifier.fillMaxWidth(),
roll = it roll = it
) )
} }
@ -104,32 +96,3 @@ private fun RollHistoryContent(
) )
} }
@Composable
private fun RollItem(
modifier: Modifier = Modifier,
roll: RollItemUio,
) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.body1,
fontWeight = FontWeight.Thin,
text = roll.from,
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.body1,
fontWeight = FontWeight.Light,
text = roll.label,
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.body1,
fontWeight = FontWeight.Bold,
text = "${roll.roll}",
)
}
}

View file

@ -12,8 +12,8 @@ class RollHistoryViewModel : ViewModel() {
private val repository = RollHistoryRepository private val repository = RollHistoryRepository
private val _rolls = mutableStateOf((emptyList<RollItemUio>())) private val _rolls = mutableStateOf((emptyList<RollHistoryItemUio>()))
val rolls: State<List<RollItemUio>> get() = _rolls val rolls: State<List<RollHistoryItemUio>> get() = _rolls
init { init {
viewModelScope.launch { viewModelScope.launch {
@ -22,10 +22,13 @@ class RollHistoryViewModel : ViewModel() {
_rolls.value = _rolls.value.toMutableList().apply { _rolls.value = _rolls.value.toMutableList().apply {
add( add(
index = 0, index = 0,
element = RollItemUio( element = RollHistoryItemUio(
from = it.from, from = it.from,
label = content.label, skillLabel = content.skillLabel,
roll = content.roll rollDifficulty = content.rollDifficulty,
resultLabel = content.resultLabel,
rollValue = content.rollValue,
rollSuccessLimit = content.rollSuccessLimit,
) )
) )
} }

View file

@ -0,0 +1,14 @@
package com.pixelized.desktop.lwa.utils
import androidx.compose.foundation.interaction.Interaction
import androidx.compose.foundation.interaction.MutableInteractionSource
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
class DisableInteractionSource : MutableInteractionSource {
override val interactions: Flow<Interaction> = emptyFlow()
override suspend fun emit(interaction: Interaction) = Unit
override fun tryEmit(interaction: Interaction): Boolean = false
}

View file

@ -6,7 +6,7 @@ junit = "4.13.2"
compose-multiplatform = "1.7.0" compose-multiplatform = "1.7.0"
androidx-lifecycle = "2.8.3" androidx-lifecycle = "2.8.3"
androidx-navigation = "2.8.0-alpha10" androidx-navigation = "2.8.0-alpha10"
ktor_version = "3.0.1" ktor_version = "3.0.0"
[plugins] [plugins]
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" } composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }
@ -24,6 +24,7 @@ androidx-lifecycle-runtime-compose = { group = "org.jetbrains.androidx.lifecycle
androidx-navigation-compose = { group = "org.jetbrains.androidx.navigation", name = "navigation-compose", version.ref = "androidx-navigation" } androidx-navigation-compose = { group = "org.jetbrains.androidx.navigation", name = "navigation-compose", version.ref = "androidx-navigation" }
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" }
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinx-json" } kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinx-json" }
ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor_version" } ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor_version" }
ktor-client-cio = { group = 'io.ktor', name = "ktor-client-cio", version.ref = "ktor_version" } ktor-client-cio = { group = 'io.ktor', name = "ktor-client-cio", version.ref = "ktor_version" }
ktor-client-websockets = { group = 'io.ktor', name = "ktor-client-websockets", version.ref = "ktor_version" } ktor-client-websockets = { group = 'io.ktor', name = "ktor-client-websockets", version.ref = "ktor_version" }