Add characteristic to the campagin detail panel.

This commit is contained in:
Thomas Andres Gomez 2025-02-26 17:13:47 +01:00
parent 29747dcb5c
commit b6b135cd40
12 changed files with 690 additions and 331 deletions

View file

@ -9,6 +9,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.BlurredEdgeTreatment
import androidx.compose.ui.draw.blur
@ -36,6 +37,20 @@ class BlurContentController(
}
}
@Composable
@Stable
fun rememberBlurContentController(
blurred: Boolean = false,
blurredRadius: Dp = 8.dp,
scrimColor: Color = LwaColorPalette.DefaultScrimColor,
) = remember {
BlurContentController(
blurred = blurred,
blurredRadius = blurredRadius,
scrimColor = scrimColor,
)
}
@Composable
fun BlurContent(
modifier: Modifier = Modifier,

View file

@ -33,9 +33,11 @@ class CharacterDetailCharacteristicDialogViewModel(
}
suspend fun showSubCharacteristicDialog(
characterInstanceId: Campaign.CharacterInstance.Id,
characterInstanceId: Campaign.CharacterInstance.Id?,
characteristic: Characteristic,
) {
if (characterInstanceId == null) return
val sheet: CharacterSheet? = characterSheetRepository.characterDetail(
characterSheetId = characterInstanceId.characterSheetId,
)

View file

@ -1,5 +1,11 @@
package com.pixelized.desktop.lwa.ui.screen.campaign
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
@ -7,12 +13,14 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.Key
@ -25,20 +33,29 @@ import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.composable.blur.BlurContent
import com.pixelized.desktop.lwa.ui.composable.blur.rememberBlurContentController
import com.pixelized.desktop.lwa.ui.composable.character.characteristic.CharacterDetailCharacteristicDialogViewModel
import com.pixelized.desktop.lwa.ui.composable.character.characteristic.CharacterSheetCharacteristicDialog
import com.pixelized.desktop.lwa.ui.composable.key.KeyHandler
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetail
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailPanel
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDiminishedViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.PlayerRibbon
import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.dialog.DiminishedStatDialog
import com.pixelized.desktop.lwa.ui.screen.network.NetworkViewModel
import com.pixelized.desktop.lwa.ui.screen.roll.RollPage
import com.pixelized.desktop.lwa.ui.screen.roll.RollViewModel
import kotlinx.coroutines.launch
import org.koin.compose.viewmodel.koinViewModel
@Composable
fun CampaignScreen(
characterDetailViewModel: CharacterDetailViewModel = koinViewModel(),
characteristicDialogViewModel: CharacterDetailCharacteristicDialogViewModel = koinViewModel(),
dismissedViewModel: CharacterDiminishedViewModel = koinViewModel(),
networkViewModel: NetworkViewModel = koinViewModel(),
rollViewModel: RollViewModel = koinViewModel(),
) {
LaunchedEffect(Unit) {
networkViewModel.connect()
@ -55,48 +72,105 @@ fun CampaignScreen(
}
}
Surface {
CampaignScreenLayout(
val scope = rememberCoroutineScope()
val blurController = rememberBlurContentController()
Surface(
modifier = Modifier.fillMaxSize(),
) {
BlurContent(
modifier = Modifier.fillMaxSize(),
top = {
Surface(
modifier = Modifier
.height(32.dp)
.fillMaxWidth(),
elevation = 1.dp,
) {
controller = blurController
) {
CampaignScreenLayout(
modifier = Modifier.fillMaxSize(),
top = {
Surface(
modifier = Modifier
.height(32.dp)
.fillMaxWidth(),
elevation = 1.dp,
) {
}
},
bottom = {
Surface(
modifier = Modifier
.height(48.dp)
.fillMaxWidth(),
elevation = 1.dp,
) {
}
},
bottom = {
Surface(
modifier = Modifier
.height(48.dp)
.fillMaxWidth(),
elevation = 1.dp,
) {
}
},
main = {
}
},
main = {
},
leftOverlay = {
PlayerRibbon(
modifier = Modifier.fillMaxHeight(),
onCharacter = {
characterDetailViewModel.showCharacter(id = it)
},
)
},
rightOverlay = {
CharacterDetailPanel(
modifier = Modifier
.width(width = 128.dp * 4)
.fillMaxHeight()
.padding(all = 8.dp),
blurController = blurController,
detailViewModel = characterDetailViewModel,
rollViewModel = rollViewModel,
characterDiminishedViewModel = dismissedViewModel,
characteristicDialogViewModel = characteristicDialogViewModel,
)
},
)
}
AnimatedContent(
modifier = Modifier.fillMaxSize(),
targetState = rollViewModel.displayOverlay.value,
transitionSpec = {
val enter = fadeIn() + slideInVertically { 64 }
val exit = fadeOut() + slideOutVertically { 64 }
enter togetherWith exit
},
leftOverlay = {
PlayerRibbon(
modifier = Modifier.fillMaxHeight(),
onCharacter = {
characterDetailViewModel.showCharacter(id = it)
) { roll ->
when (roll) {
true -> RollPage(
viewModel = rollViewModel,
onDismissRequest = {
blurController.hide()
rollViewModel.hideOverlay()
},
)
},
rightOverlay = {
CharacterDetail(
modifier = Modifier
.padding(all = 8.dp)
.fillMaxHeight(),
detailViewModel = characterDetailViewModel,
dismissedViewModel = dismissedViewModel,
else -> Box(
modifier = Modifier.fillMaxSize()
)
}
}
CharacterSheetCharacteristicDialog(
dialog = characteristicDialogViewModel.statChangeDialog,
onConfirm = { dialog ->
scope.launch {
characteristicDialogViewModel.changeSubCharacteristic(
characterInstanceId = dialog.characterInstanceId,
characteristic = dialog.characteristic,
value = dialog.value().text.toIntOrNull() ?: 0,
)
characteristicDialogViewModel.hideSubCharacteristicDialog()
blurController.hide()
}
},
onDismissRequest = {
characteristicDialogViewModel.hideSubCharacteristicDialog()
blurController.hide()
},
)

View file

@ -6,141 +6,150 @@ import androidx.compose.animation.ExitTransition
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.composable.blur.BlurContentController
import com.pixelized.desktop.lwa.ui.composable.character.characteristic.CharacterDetailCharacteristicDialogViewModel
import com.pixelized.desktop.lwa.ui.composable.character.characteristic.CharacterSheetCharacteristicDialog
import com.pixelized.desktop.lwa.ui.composable.character.characteristic.CharacterSheetCharacteristicDialogUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.header.CharacterDetailHeader
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.header.CharacterDetailHeaderUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.CharacterDetailSheet
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.CharacterDetailSheetCharacteristicUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.CharacterDetailSheetUio
import com.pixelized.desktop.lwa.ui.screen.roll.RollViewModel
import com.pixelized.desktop.lwa.ui.theme.lwa
import com.pixelized.shared.lwa.model.campaign.Campaign
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.ic_close_24dp
import lwacharactersheet.composeapp.generated.resources.ic_heart_24dp
import lwacharactersheet.composeapp.generated.resources.ic_near_me
import lwacharactersheet.composeapp.generated.resources.ic_skull_24dp
import lwacharactersheet.composeapp.generated.resources.ic_water_drop_24dp
import org.jetbrains.compose.resources.painterResource
import org.koin.compose.viewmodel.koinViewModel
@Stable
data class CharacterDetailHeaderUio(
val id: Campaign.CharacterInstance.Id,
val portrait: String?,
val name: String,
val hp: String,
val maxHp: String,
val pp: String,
val maxPp: String,
val mov: String,
data class CharacterDetailPanelUio(
val characterInstanceId: Campaign.CharacterInstance.Id?,
val header: StateFlow<CharacterDetailHeaderUio?>,
val sheet: StateFlow<CharacterDetailSheetUio?>,
)
@Composable
fun CharacterDetail(
fun CharacterDetailPanel(
modifier: Modifier = Modifier,
dismissedViewModel: CharacterDiminishedViewModel,
detailViewModel: CharacterDetailViewModel = koinViewModel(),
characteristicDialogViewModel: CharacterDetailCharacteristicDialogViewModel = koinViewModel(),
blurController: BlurContentController,
detailViewModel: CharacterDetailViewModel,
characteristicDialogViewModel: CharacterDetailCharacteristicDialogViewModel,
characterDiminishedViewModel: CharacterDiminishedViewModel,
rollViewModel: RollViewModel,
) {
val blurController = remember { BlurContentController() }
val scope = rememberCoroutineScope()
val detail = detailViewModel.detail.collectAsState()
val detail: State<CharacterDetailPanelUio> = detailViewModel.detail.collectAsState()
AnimatedContent(
CharacterDetailAnimatedPanel(
modifier = modifier,
targetState = detail.value,
transitionSpec = {
if (initialState?.id != targetState?.id) {
(fadeIn() + slideInHorizontally { it / 2 }).togetherWith(fadeOut())
} else {
EnterTransition.None togetherWith ExitTransition.None
detail = detail,
onDismissRequest = {
detailViewModel.hideCharacter()
},
onDiminished = {
scope.launch {
characterDiminishedViewModel.showDiminishedDialog(
characterInstanceId = it
)
}
}
) {
when (it) {
null -> Box(
modifier = Modifier.fillMaxHeight(),
},
onHp = {
scope.launch {
blurController.show()
characteristicDialogViewModel.showSubCharacteristicDialog(
characterInstanceId = it,
characteristic = Campaign.CharacterInstance.Characteristic.Damage,
)
}
},
onPp = {
scope.launch {
blurController.show()
characteristicDialogViewModel.showSubCharacteristicDialog(
characterInstanceId = it,
characteristic = Campaign.CharacterInstance.Characteristic.Power,
)
}
},
onCharacteristic = {
rollViewModel.prepareRoll(
characterSheetId = detail.value.characterInstanceId?.characterSheetId!!,
label = it.label,
rollAction = "1d100",
rollSuccessValue = (it.value.toIntOrNull() ?: 0) * 5,
)
blurController.show()
rollViewModel.showOverlay()
},
)
}
else -> {
Box(
modifier = Modifier
.fillMaxHeight()
.width(width = 128.dp * 4),
) {
CharacterDetailContent(
@Composable
fun CharacterDetailAnimatedPanel(
modifier: Modifier = Modifier,
detail: State<CharacterDetailPanelUio>,
onDismissRequest: (id: Campaign.CharacterInstance.Id) -> Unit,
onDiminished: (id: Campaign.CharacterInstance.Id) -> Unit,
onHp: (id: Campaign.CharacterInstance.Id) -> Unit,
onPp: (id: Campaign.CharacterInstance.Id) -> Unit,
onCharacteristic: (CharacterDetailSheetCharacteristicUio) -> Unit,
) {
Box(
modifier = modifier,
) {
AnimatedContent(
modifier = Modifier.matchParentSize(),
targetState = detail.value,
transitionSpec = {
if (initialState.characterInstanceId != targetState.characterInstanceId) {
val enter = fadeIn() + slideInHorizontally { it / 2 }
val exit = fadeOut() + slideOutHorizontally { it / 2 }
enter togetherWith exit
} else {
EnterTransition.None togetherWith ExitTransition.None
}
}
) {
when {
it.characterInstanceId == null -> Box(
modifier = Modifier.matchParentSize(),
)
else -> {
Box(
modifier = Modifier.matchParentSize(),
character = it,
onDismissRequest = {
detailViewModel.hideCharacter()
},
onDiminished = {
scope.launch {
dismissedViewModel.showDiminishedDialog(id = it.id)
}
},
onHp = {
scope.launch {
characteristicDialogViewModel.showSubCharacteristicDialog(
characterInstanceId = it.id,
characteristic = Campaign.CharacterInstance.Characteristic.Damage,
)
}
},
onPp = {
scope.launch {
characteristicDialogViewModel.showSubCharacteristicDialog(
characterInstanceId = it.id,
characteristic = Campaign.CharacterInstance.Characteristic.Power,
)
}
},
)
CharacterSheetCharacteristicDialog(
dialog = characteristicDialogViewModel.statChangeDialog,
onConfirm = { dialog ->
scope.launch {
characteristicDialogViewModel.changeSubCharacteristic(
characterInstanceId = dialog.characterInstanceId,
characteristic = dialog.characteristic,
value = dialog.value().text.toIntOrNull() ?: 0,
)
characteristicDialogViewModel.hideSubCharacteristicDialog()
blurController.hide()
}
},
onDismissRequest = {
characteristicDialogViewModel.hideSubCharacteristicDialog()
blurController.hide()
}
)
) {
CharacterDetailContent(
modifier = Modifier.matchParentSize(),
header = it.header.collectAsState(),
sheet = it.sheet.collectAsState(),
onDismissRequest = { onDismissRequest(it.characterInstanceId) },
onDiminished = { onDiminished(it.characterInstanceId) },
onHp = { onHp(it.characterInstanceId) },
onPp = { onPp(it.characterInstanceId) },
onCharacteristic = onCharacteristic,
)
}
}
}
}
@ -150,169 +159,35 @@ fun CharacterDetail(
@Composable
fun CharacterDetailContent(
modifier: Modifier = Modifier,
character: CharacterDetailHeaderUio,
header: State<CharacterDetailHeaderUio?>,
sheet: State<CharacterDetailSheetUio?>,
onDismissRequest: () -> Unit,
onDiminished: () -> Unit,
onHp: () -> Unit,
onPp: () -> Unit,
) {
Box(
modifier = modifier,
) {
Background(
character = character,
)
Column {
CharacterHeader(
modifier = Modifier.padding(start = 16.dp).fillMaxWidth(),
character = character,
onDismissRequest = onDismissRequest,
onDiminished = onDiminished,
onHp = onHp,
onPp = onPp,
)
}
}
}
@Composable
private fun Background(
modifier: Modifier = Modifier,
character: CharacterDetailHeaderUio,
onCharacteristic: (CharacterDetailSheetCharacteristicUio) -> Unit,
) {
Surface(
modifier = modifier.fillMaxSize(),
color = MaterialTheme.lwa.colorScheme.elevatedSurface,
) {
// Image(
// modifier = Modifier.fillMaxSize().drawWithContent {
// drawContent()
// drawRect(
// brush = Brush.verticalGradient(
// listOfNotNull(
// color?.copy(alpha = 0.7f),
// color,
// )
// )
// )
// },
// painter = rememberAsyncImagePainter(model = character.portrait),
// contentDescription = null,
// contentScale = ContentScale.Crop,
// alignment = Alignment.TopCenter,
// )
}
}
@Composable
private fun CharacterHeader(
modifier: Modifier = Modifier,
character: CharacterDetailHeaderUio,
onDismissRequest: () -> Unit,
onDiminished: () -> Unit,
onHp: () -> Unit,
onPp: () -> Unit,
) {
Column(
modifier = modifier,
) {
Row {
Text(
modifier = Modifier.weight(1f)
.align(alignment = Alignment.CenterVertically),
style = MaterialTheme.typography.h5,
text = character.name,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
Column {
CharacterDetailHeader(
modifier = Modifier.padding(start = 16.dp).fillMaxWidth(),
header = header,
onDismissRequest = onDismissRequest,
onDiminished = onDiminished,
onHp = onHp,
onPp = onPp,
)
CharacterDetailSheet(
modifier = Modifier
.weight(1f)
.verticalScroll(state = rememberScrollState())
.padding(all = 16.dp),
sheet = sheet,
onCharacteristic = onCharacteristic,
)
IconButton(
onClick = onDiminished,
) {
Icon(
modifier = Modifier.size(size = 24.dp),
painter = painterResource(Res.drawable.ic_skull_24dp),
tint = MaterialTheme.colors.primary,
contentDescription = null,
)
}
IconButton(
onClick = onDismissRequest,
) {
Icon(
painter = painterResource(Res.drawable.ic_close_24dp),
tint = MaterialTheme.lwa.colorScheme.base.primary,
contentDescription = null,
)
}
}
Row(
horizontalArrangement = Arrangement.spacedBy(space = 12.dp),
) {
Row(
modifier = Modifier.clip(shape = CircleShape).clickable { onHp() },
verticalAlignment = Alignment.Bottom,
) {
Icon(
modifier = Modifier.padding(bottom = 4.dp, end = 2.dp).size(12.dp),
painter = painterResource(Res.drawable.ic_heart_24dp),
contentDescription = null
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.h6,
color = MaterialTheme.lwa.colorScheme.base.primary,
fontWeight = FontWeight.Bold,
text = character.hp,
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.caption,
fontWeight = FontWeight.Thin,
text = "/${character.maxHp}",
)
}
Row(
modifier = Modifier.clip(shape = CircleShape).clickable { onPp() },
verticalAlignment = Alignment.Bottom,
) {
Icon(
modifier = Modifier.padding(bottom = 4.dp, end = 2.dp).size(12.dp),
painter = painterResource(Res.drawable.ic_water_drop_24dp),
contentDescription = null
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.h6,
color = MaterialTheme.lwa.colorScheme.base.primary,
fontWeight = FontWeight.Bold,
text = character.pp,
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.caption,
fontWeight = FontWeight.Thin,
text = "/${character.maxPp}",
)
}
Row(
verticalAlignment = Alignment.Bottom,
) {
Icon(
modifier = Modifier.padding(bottom = 4.dp, end = 2.dp).size(12.dp),
painter = painterResource(Res.drawable.ic_near_me),
contentDescription = null,
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.h6,
text = character.mov,
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.caption,
text = "m",
)
}
}
}
}

View file

@ -1,16 +1,35 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.player.detail
import com.pixelized.desktop.lwa.ui.composable.tooltip.TooltipUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.header.CharacterDetailHeaderUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.CharacterDetailSheetCharacteristicUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.CharacterDetailSheetUio
import com.pixelized.shared.lwa.model.AlteredCharacterSheetFactory
import com.pixelized.shared.lwa.model.alteration.FieldAlteration
import com.pixelized.shared.lwa.model.campaign.Campaign
import com.pixelized.shared.lwa.model.campaign.damage
import com.pixelized.shared.lwa.model.campaign.power
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.character_sheet__characteristics__cha
import lwacharactersheet.composeapp.generated.resources.character_sheet__characteristics__con
import lwacharactersheet.composeapp.generated.resources.character_sheet__characteristics__dex
import lwacharactersheet.composeapp.generated.resources.character_sheet__characteristics__hei
import lwacharactersheet.composeapp.generated.resources.character_sheet__characteristics__int
import lwacharactersheet.composeapp.generated.resources.character_sheet__characteristics__pow
import lwacharactersheet.composeapp.generated.resources.character_sheet__characteristics__str
import lwacharactersheet.composeapp.generated.resources.tooltip__characteristics__charisma
import lwacharactersheet.composeapp.generated.resources.tooltip__characteristics__constitution
import lwacharactersheet.composeapp.generated.resources.tooltip__characteristics__dexterity
import lwacharactersheet.composeapp.generated.resources.tooltip__characteristics__height
import lwacharactersheet.composeapp.generated.resources.tooltip__characteristics__intelligence
import lwacharactersheet.composeapp.generated.resources.tooltip__characteristics__power
import lwacharactersheet.composeapp.generated.resources.tooltip__characteristics__strength
import org.jetbrains.compose.resources.getString
class CharacterDetailFactory(
private val alteredCharacterSheetFactory: AlteredCharacterSheetFactory,
) {
fun convertToCharacterDetailHeaderUio(
characterInstanceId: Campaign.CharacterInstance.Id,
characterSheet: CharacterSheet?,
@ -38,4 +57,80 @@ class CharacterDetailFactory(
mov = "${alteredCharacterSheet.movement}"
)
}
suspend fun convertToCharacterDetailSheetUio(
characterInstanceId: Campaign.CharacterInstance.Id,
characterSheet: CharacterSheet?,
characterInstance: Campaign.CharacterInstance,
alterations: Map<String, List<FieldAlteration>>,
): CharacterDetailSheetUio? {
if (characterSheet == null) return null
val alteredCharacterSheet = alteredCharacterSheetFactory.sheet(
characterSheet = characterSheet,
alterations = alterations,
)
return CharacterDetailSheetUio(
characterInstanceId = characterInstanceId,
characteristics = listOf(
CharacterDetailSheetCharacteristicUio(
label = getString(Res.string.character_sheet__characteristics__str),
value = "${alteredCharacterSheet.strength}",
tooltips = TooltipUio(
title = getString(Res.string.character_sheet__characteristics__str),
description = getString(Res.string.tooltip__characteristics__strength),
),
),
CharacterDetailSheetCharacteristicUio(
label = getString(Res.string.character_sheet__characteristics__dex),
value = "${alteredCharacterSheet.dexterity}",
tooltips = TooltipUio(
title = getString(Res.string.character_sheet__characteristics__dex),
description = getString(Res.string.tooltip__characteristics__dexterity),
),
),
CharacterDetailSheetCharacteristicUio(
label = getString(Res.string.character_sheet__characteristics__con),
value = "${alteredCharacterSheet.constitution}",
tooltips = TooltipUio(
title = getString(Res.string.character_sheet__characteristics__con),
description = getString(Res.string.tooltip__characteristics__constitution),
),
),
CharacterDetailSheetCharacteristicUio(
label = getString(Res.string.character_sheet__characteristics__hei),
value = "${alteredCharacterSheet.height}",
tooltips = TooltipUio(
title = getString(Res.string.character_sheet__characteristics__hei),
description = getString(Res.string.tooltip__characteristics__height),
),
),
CharacterDetailSheetCharacteristicUio(
label = getString(Res.string.character_sheet__characteristics__int),
value = "${alteredCharacterSheet.intelligence}",
tooltips = TooltipUio(
title = getString(Res.string.character_sheet__characteristics__int),
description = getString(Res.string.tooltip__characteristics__intelligence),
),
),
CharacterDetailSheetCharacteristicUio(
label = getString(Res.string.character_sheet__characteristics__pow),
value = "${alteredCharacterSheet.power}",
tooltips = TooltipUio(
title = getString(Res.string.character_sheet__characteristics__pow),
description = getString(Res.string.tooltip__characteristics__power),
),
),
CharacterDetailSheetCharacteristicUio(
label = getString(Res.string.character_sheet__characteristics__cha),
value = "${alteredCharacterSheet.charisma}",
tooltips = TooltipUio(
title = getString(Res.string.character_sheet__characteristics__cha),
description = getString(Res.string.tooltip__characteristics__charisma),
),
),
)
)
}
}

View file

@ -6,13 +6,11 @@ import com.pixelized.desktop.lwa.repository.alteration.AlterationRepository
import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.shared.lwa.model.campaign.Campaign
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
class CharacterDetailViewModel(
@ -24,32 +22,50 @@ class CharacterDetailViewModel(
private val displayedCharacterId = MutableStateFlow<Campaign.CharacterInstance.Id?>(null)
@OptIn(ExperimentalCoroutinesApi::class)
val detail: StateFlow<CharacterDetailHeaderUio?> = displayedCharacterId
.flatMapLatest { characterInstanceId ->
if (characterInstanceId != null) {
campaignRepository
.characterInstanceFlow(id = characterInstanceId)
.flatMapLatest { characterInstance ->
combine(
characterSheetRepository.characterDetailFlow(characterId = characterInstanceId.characterSheetId),
alterationRepository.alterationsFlow(characterId = characterInstanceId),
) { characterSheet, alterations ->
characterDetailFactory.convertToCharacterDetailHeaderUio(
characterInstanceId = characterInstanceId,
characterSheet = characterSheet,
characterInstance = characterInstance,
alterations = alterations,
)
}
}
} else {
flowOf(null)
}
}.stateIn(
val detail: StateFlow<CharacterDetailPanelUio> = displayedCharacterId
.map { characterInstanceId ->
if (characterInstanceId == null) return@map empty()
CharacterDetailPanelUio(
characterInstanceId = characterInstanceId,
header = combine(
campaignRepository.characterInstanceFlow(id = characterInstanceId),
characterSheetRepository.characterDetailFlow(characterId = characterInstanceId.characterSheetId),
alterationRepository.alterationsFlow(characterId = characterInstanceId),
) { characterInstance, characterSheet, alterations ->
characterDetailFactory.convertToCharacterDetailHeaderUio(
characterInstanceId = characterInstanceId,
characterSheet = characterSheet,
characterInstance = characterInstance,
alterations = alterations,
)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = null,
),
sheet = combine(
campaignRepository.characterInstanceFlow(id = characterInstanceId),
characterSheetRepository.characterDetailFlow(characterId = characterInstanceId.characterSheetId),
alterationRepository.alterationsFlow(characterId = characterInstanceId),
) { characterInstance, characterSheet, alterations ->
characterDetailFactory.convertToCharacterDetailSheetUio(
characterInstanceId = characterInstanceId,
characterSheet = characterSheet,
characterInstance = characterInstance,
alterations = alterations,
)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = null,
),
)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = null,
initialValue = empty(),
)
fun showCharacter(id: Campaign.CharacterInstance.Id) {
@ -59,4 +75,10 @@ class CharacterDetailViewModel(
fun hideCharacter() {
displayedCharacterId.value = null
}
private fun empty() = CharacterDetailPanelUio(
characterInstanceId = null,
header = MutableStateFlow(null),
sheet = MutableStateFlow(null),
)
}

View file

@ -20,14 +20,16 @@ class CharacterDiminishedViewModel(
val diminishedDialog: State<DiminishedStatDialogUio?> get() = _diminishedDialog
suspend fun showDiminishedDialog(
id: Campaign.CharacterInstance.Id,
characterInstanceId: Campaign.CharacterInstance.Id?,
) {
if (characterInstanceId == null) return
val diminished = 0 // TODO repository.characterDiminishedFlow(id = id).value
val textFieldValue = mutableStateOf(
TextFieldValue("$diminished", selection = TextRange(index = 0))
)
_diminishedDialog.value = DiminishedStatDialogUio(
id = id,
id = characterInstanceId,
label = getString(resource = Res.string.character_sheet__diminished__label),
value = { textFieldValue.value },
onValueChange = { value ->
@ -51,6 +53,4 @@ class CharacterDiminishedViewModel(
// diminished = value,
// )
}
}

View file

@ -0,0 +1,156 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.header
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
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.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.theme.lwa
import com.pixelized.shared.lwa.model.campaign.Campaign
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.ic_close_24dp
import lwacharactersheet.composeapp.generated.resources.ic_heart_24dp
import lwacharactersheet.composeapp.generated.resources.ic_near_me
import lwacharactersheet.composeapp.generated.resources.ic_skull_24dp
import lwacharactersheet.composeapp.generated.resources.ic_water_drop_24dp
import org.jetbrains.compose.resources.painterResource
@Stable
data class CharacterDetailHeaderUio(
val id: Campaign.CharacterInstance.Id,
val portrait: String?,
val name: String,
val hp: String,
val maxHp: String,
val pp: String,
val maxPp: String,
val mov: String,
)
@Composable
fun CharacterDetailHeader(
modifier: Modifier = Modifier,
header: State<CharacterDetailHeaderUio?>,
onDismissRequest: () -> Unit,
onDiminished: () -> Unit,
onHp: () -> Unit,
onPp: () -> Unit,
) {
Column(
modifier = modifier,
) {
Row {
Text(
modifier = Modifier.weight(1f)
.align(alignment = Alignment.CenterVertically),
style = MaterialTheme.typography.h5,
text = header.value?.name ?: "",
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
IconButton(
onClick = onDiminished,
) {
Icon(
modifier = Modifier.size(size = 24.dp),
painter = painterResource(Res.drawable.ic_skull_24dp),
tint = MaterialTheme.colors.primary,
contentDescription = null,
)
}
IconButton(
onClick = onDismissRequest,
) {
Icon(
painter = painterResource(Res.drawable.ic_close_24dp),
tint = MaterialTheme.lwa.colorScheme.base.primary,
contentDescription = null,
)
}
}
Row(
horizontalArrangement = Arrangement.spacedBy(space = 12.dp),
) {
Row(
modifier = Modifier.clip(shape = CircleShape).clickable { onHp() },
verticalAlignment = Alignment.Bottom,
) {
Icon(
modifier = Modifier.padding(bottom = 4.dp, end = 2.dp).size(12.dp),
painter = painterResource(Res.drawable.ic_heart_24dp),
contentDescription = null
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.h6,
color = MaterialTheme.lwa.colorScheme.base.primary,
fontWeight = FontWeight.Bold,
text = header.value?.hp ?: "",
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.caption,
fontWeight = FontWeight.Thin,
text = "/${header.value?.maxHp ?: ""}",
)
}
Row(
modifier = Modifier.clip(shape = CircleShape).clickable { onPp() },
verticalAlignment = Alignment.Bottom,
) {
Icon(
modifier = Modifier.padding(bottom = 4.dp, end = 2.dp).size(12.dp),
painter = painterResource(Res.drawable.ic_water_drop_24dp),
contentDescription = null
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.h6,
color = MaterialTheme.lwa.colorScheme.base.primary,
fontWeight = FontWeight.Bold,
text = header.value?.pp ?: "",
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.caption,
fontWeight = FontWeight.Thin,
text = "/${header.value?.maxPp ?: ""}",
)
}
Row(
verticalAlignment = Alignment.Bottom,
) {
Icon(
modifier = Modifier.padding(bottom = 4.dp, end = 2.dp).size(12.dp),
painter = painterResource(Res.drawable.ic_near_me),
contentDescription = null,
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.h6,
text = header.value?.mov ?: "",
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.caption,
text = "m",
)
}
}
}
}

View file

@ -0,0 +1,61 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
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.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.composable.decoratedBox.DecoratedBox
import com.pixelized.desktop.lwa.ui.composable.tooltip.TooltipLayout
import com.pixelized.desktop.lwa.ui.composable.tooltip.TooltipUio
@Stable
data class CharacterDetailSheetCharacteristicUio(
val value: String,
val label: String,
val tooltips: TooltipUio?,
)
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun CharacterDetailSheetCharacteristic(
modifier: Modifier = Modifier,
paddingValues: PaddingValues = PaddingValues(all = 8.dp),
characteristic: CharacterDetailSheetCharacteristicUio,
onClick: () -> Unit,
) {
TooltipLayout(
tooltip = characteristic.tooltips,
content = {
DecoratedBox(
modifier = Modifier
.clickable(onClick = onClick)
.padding(paddingValues = paddingValues)
.then(other = modifier),
) {
Text(
modifier = Modifier.align(alignment = Alignment.TopCenter),
style = MaterialTheme.typography.caption,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = characteristic.label,
)
Text(
modifier = Modifier.align(alignment = Alignment.Center),
style = MaterialTheme.typography.h3,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
color = MaterialTheme.colors.primary,
text = characteristic.value
)
}
},
)
}

View file

@ -0,0 +1,47 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.pixelized.shared.lwa.model.campaign.Campaign
@Stable
data class CharacterDetailSheetUio(
val characterInstanceId: Campaign.CharacterInstance.Id,
val characteristics: List<CharacterDetailSheetCharacteristicUio>,
)
@Composable
fun CharacterDetailSheet(
modifier: Modifier = Modifier,
sheet: State<CharacterDetailSheetUio?>,
onCharacteristic: (CharacterDetailSheetCharacteristicUio) -> Unit,
) {
Row(
modifier = modifier
) {
Column(
verticalArrangement = Arrangement.spacedBy(space = 8.dp)
) {
sheet.value?.characteristics?.forEach {
CharacterDetailSheetCharacteristic(
modifier = Modifier.size(width = 80.dp, height = 120.dp),
characteristic = it,
onClick = { onCharacteristic(it) },
)
}
}
Column(
verticalArrangement = Arrangement.spacedBy(space = 8.dp)
) {
}
}
}

View file

@ -6,13 +6,13 @@ import androidx.compose.animation.core.spring
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import com.pixelized.shared.lwa.usecase.ExpressionUseCase
import com.pixelized.shared.lwa.usecase.SkillStepUseCase
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.desktop.lwa.repository.roll_history.RollHistoryRepository
import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.CharacterSheetPageUio
import com.pixelized.desktop.lwa.ui.screen.roll.DifficultyUio.Difficulty
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet
import com.pixelized.shared.lwa.usecase.ExpressionUseCase
import com.pixelized.shared.lwa.usecase.SkillStepUseCase
import kotlinx.coroutines.Job
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
@ -55,13 +55,17 @@ class RollViewModel(
private val _rollDifficulty = mutableStateOf<DifficultyUio?>(null)
val rollDifficulty: State<DifficultyUio?> get() = _rollDifficulty
private val _displayOverlay = mutableStateOf(false)
val displayOverlay: State<Boolean> get() = _displayOverlay
fun prepareRoll(
sheet: CharacterSheetPageUio,
characteristic: CharacterSheetPageUio.Characteristic,
) {
val diminished = 0 // TODO characterSheetRepository.characterDiminishedFlow(id = sheet.id).value
val diminished =
0 // TODO characterSheetRepository.characterDiminishedFlow(id = sheet.id).value
prepareRoll(
sheet = sheet,
characterSheetId = sheet.id,
label = characteristic.label,
rollAction = "1d100",
rollSuccessValue = (characteristic.value.toIntOrNull() ?: 0) * 5 - diminished,
@ -73,7 +77,7 @@ class RollViewModel(
node: CharacterSheetPageUio.Node,
) {
prepareRoll(
sheet = sheet,
characterSheetId = sheet.id,
label = node.label,
rollAction = "1d100",
rollSuccessValue = node.value,
@ -85,15 +89,15 @@ class RollViewModel(
roll: CharacterSheetPageUio.Roll,
) {
prepareRoll(
sheet = sheet,
characterSheetId = sheet.id,
label = roll.label,
rollAction = roll.value,
rollSuccessValue = null,
)
}
private fun prepareRoll(
sheet: CharacterSheetPageUio,
fun prepareRoll(
characterSheetId: String,
label: String,
rollAction: String,
rollSuccessValue: Int?,
@ -101,7 +105,7 @@ class RollViewModel(
this.sheet = runBlocking {
rollRotation.snapTo(0f)
rollScale.snapTo(1f)
characterSheetRepository.characterDetail(characterSheetId = sheet.id)!!
characterSheetRepository.characterDetail(characterSheetId = characterSheetId)!!
}
this.rollAction = rollAction
@ -238,4 +242,12 @@ class RollViewModel(
value = rollStep?.success?.last
)
}
fun showOverlay() {
_displayOverlay.value = true
}
fun hideOverlay() {
_displayOverlay.value = false
}
}

View file

@ -59,7 +59,7 @@ class RollUseCase {
if (quantity > 1 && left != 1) print(",")
}
}.also {
print("}")
println("}")
}
}