Add characteristic + common / special / magic skill to detail panel.

This commit is contained in:
Thomas Andres Gomez 2025-02-27 00:14:10 +01:00
parent b6b135cd40
commit b6d02c21be
28 changed files with 633 additions and 135 deletions

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M491,621q70,0 119,-45t49,-109q0,-57 -36.5,-96.5T534,331q-47,0 -79.5,30T422,435q0,19 7.5,37t21.5,33l57,-57q-3,-2 -4.5,-5t-1.5,-7q0,-11 9,-17.5t23,-6.5q20,0 33,16.5t13,39.5q0,31 -25.5,52.5T492,542q-47,0 -79.5,-38T380,411q0,-29 11,-55.5t31,-46.5l-57,-57q-32,31 -49,72t-17,86q0,88 56,149.5T491,621ZM240,880v-172q-57,-52 -88.5,-121.5T120,440q0,-150 105,-255t255,-105q125,0 221.5,73.5T827,345l52,205q5,19 -7,34.5T840,600h-80v120q0,33 -23.5,56.5T680,800h-80v80h-80v-160h160v-200h108l-38,-155q-23,-91 -98,-148t-172,-57q-116,0 -198,81t-82,197q0,60 24.5,114t69.5,96l26,24v208h-80ZM494,520Z"
android:fillColor="#5f6368"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M440,459ZM440,840L313,726q-72,-65 -123.5,-116t-85,-96q-33.5,-45 -49,-87T40,339q0,-94 63,-156.5T260,120q52,0 99,22t81,62q34,-40 81,-62t99,-22q81,0 136,45.5T831,280h-85q-18,-40 -53,-60t-73,-20q-51,0 -88,27.5T463,300h-46q-31,-45 -70.5,-72.5T260,200q-57,0 -98.5,39.5T120,339q0,33 14,67t50,78.5q36,44.5 98,104T440,732q26,-23 61,-53t56,-50l9,9 19.5,19.5L605,677l9,9q-22,20 -56,49.5T498,788l-58,52ZM720,680v-120L600,560v-80h120v-120h80v120h120v80L800,560v120h-80Z"
android:fillColor="#5f6368"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M480,880q-139,-35 -229.5,-159.5T160,444v-244l320,-120 320,120v244q0,152 -90.5,276.5T480,880ZM480,796q104,-33 172,-132t68,-220v-189l-240,-90 -240,90v189q0,121 68,220t172,132ZM480,480Z"
android:fillColor="#5f6368"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M762,864 L645,748l-88,88 -28,-28q-23,-23 -23,-57t23,-57l169,-169q23,-23 57,-23t57,23l28,28 -88,88 116,117q12,12 12,28t-12,28l-50,50q-12,12 -28,12t-28,-12ZM880,236L426,690l5,4q23,23 23,57t-23,57l-28,28 -88,-88L198,864q-12,12 -28,12t-28,-12l-50,-50q-12,-12 -12,-28t12,-28l116,-117 -88,-88 28,-28q23,-23 57,-23t57,23l4,5 454,-454h160v160ZM334,377l24,-23 23,-24 -23,24 -24,23ZM278,434L80,236v-160h160l198,198 -57,56 -174,-174h-47v47l174,174 -56,57ZM370,633 L800,203v-47h-47L323,586l47,47ZM370,633 L346,610 323,586 346,610 370,633Z"
android:fillColor="#5f6368"/>
</vector>

View file

@ -15,6 +15,7 @@ import com.pixelized.desktop.lwa.repository.settings.SettingsRepository
import com.pixelized.desktop.lwa.repository.settings.SettingsStore import com.pixelized.desktop.lwa.repository.settings.SettingsStore
import com.pixelized.desktop.lwa.ui.composable.character.characteristic.CharacterDetailCharacteristicDialogViewModel import com.pixelized.desktop.lwa.ui.composable.character.characteristic.CharacterDetailCharacteristicDialogViewModel
import com.pixelized.desktop.lwa.ui.composable.character.characteristic.CharacterSheetCharacteristicDialogFactory import com.pixelized.desktop.lwa.ui.composable.character.characteristic.CharacterSheetCharacteristicDialogFactory
import com.pixelized.desktop.lwa.ui.screen.campaign.CampaignViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailFactory import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailFactory
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailViewModel 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.detail.CharacterDiminishedViewModel
@ -106,6 +107,7 @@ val factoryDependencies
val viewModelDependencies val viewModelDependencies
get() = module { get() = module {
viewModelOf(::CampaignViewModel)
viewModelOf(::MainPageViewModel) viewModelOf(::MainPageViewModel)
viewModelOf(::CharacterSheetViewModel) viewModelOf(::CharacterSheetViewModel)
viewModelOf(::CharacterSheetEditViewModel) viewModelOf(::CharacterSheetEditViewModel)

View file

@ -2,7 +2,7 @@ package com.pixelized.desktop.lwa.repository.alteration
import com.pixelized.shared.lwa.model.alteration.Alteration import com.pixelized.shared.lwa.model.alteration.Alteration
import com.pixelized.shared.lwa.model.alteration.FieldAlteration import com.pixelized.shared.lwa.model.alteration.FieldAlteration
import com.pixelized.shared.lwa.model.campaign.Campaign import com.pixelized.shared.lwa.model.campaign.Campaign.CharacterInstance
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -20,7 +20,7 @@ class AlterationRepository(
private val store: AlterationStore, private val store: AlterationStore,
) { ) {
private val scope = CoroutineScope(Dispatchers.IO + Job()) private val scope = CoroutineScope(Dispatchers.IO + Job())
private val activeAlterationMapFlow: StateFlow<Map<Campaign.CharacterInstance.Id, Map<String, List<FieldAlteration>>>> = private val activeAlterationMapFlow: StateFlow<Map<CharacterInstance.Id, Map<String, List<FieldAlteration>>>> =
combine( combine(
store.alterations, store.alterations,
store.active, store.active,
@ -38,17 +38,36 @@ class AlterationRepository(
) )
fun alterationsFlow( fun alterationsFlow(
characterId: Campaign.CharacterInstance.Id, characterId: CharacterInstance.Id,
): Flow<Map<String, List<FieldAlteration>>> { ): Flow<Map<String, List<FieldAlteration>>> {
return activeAlterationMapFlow.map { it[characterId] ?: emptyMap() } return activeAlterationMapFlow.map { it[characterId] ?: emptyMap() }
} }
fun alterations( fun alterations(
characterInstanceId: Campaign.CharacterInstance.Id, characterInstanceId: CharacterInstance.Id,
): Map<String, List<FieldAlteration>> { ): Map<String, List<FieldAlteration>> {
return activeAlterationMapFlow.value[characterInstanceId] ?: emptyMap() return activeAlterationMapFlow.value[characterInstanceId] ?: emptyMap()
} }
suspend fun updateActiveAlterations(
characterInstanceId: CharacterInstance.Id,
) {
store.updateActiveAlterations(
characterInstanceId = characterInstanceId,
)
}
suspend fun toggleActiveAlteration(
characterInstanceId: CharacterInstance.Id,
alterationId: String,
) {
// alteration was active for the character toggle it off.
store.toggleActiveAlteration(
characterInstance = characterInstanceId,
alterationId = alterationId,
)
}
private fun transformToAlterationFieldMap( private fun transformToAlterationFieldMap(
alterations: Map<String, Alteration>, alterations: Map<String, Alteration>,
actives: List<String>, actives: List<String>,
@ -71,15 +90,4 @@ class AlterationRepository(
} }
return fieldAlterations return fieldAlterations
} }
suspend fun toggleActiveAlteration(
characterInstanceId: Campaign.CharacterInstance.Id,
alterationId: String,
) {
// alteration was active for the character toggle it off.
store.toggleActiveAlteration(
characterInstance = characterInstanceId,
alterationId = alterationId,
)
}
} }

View file

@ -41,6 +41,16 @@ class AlterationStore(
_alterations.value = loadAlteration() _alterations.value = loadAlteration()
} }
suspend fun updateActiveAlterations(
characterInstanceId: CharacterInstance.Id,
) {
_active.value = _active.value.toMutableMap().also {
it[characterInstanceId] = loadActiveAlterations(
characterInstanceId = characterInstanceId,
)
}
}
private suspend fun loadAlteration(): Map<String, Alteration> { private suspend fun loadAlteration(): Map<String, Alteration> {
val request = client.alterations() val request = client.alterations()
val data = request.map { alterationFactory.convertFromJson(json = it) } val data = request.map { alterationFactory.convertFromJson(json = it) }
@ -54,9 +64,6 @@ class AlterationStore(
characterSheetId = characterInstanceId.characterSheetId, characterSheetId = characterInstanceId.characterSheetId,
instanceId = characterInstanceId.instanceId, instanceId = characterInstanceId.instanceId,
) )
_active.value = _active.value.toMutableMap().also {
it[characterInstanceId] = request
}
return request return request
} }

View file

@ -109,8 +109,9 @@ class CharacterSheetStore(
is UpdateSkillUsageMessage -> { is UpdateSkillUsageMessage -> {
updateCharacterSkillChange( updateCharacterSkillChange(
characterId = payload.characterId, characterId = payload.characterSheetId,
skillId = payload.skillId, skillId = payload.skillId,
used = payload.used,
) )
} }
@ -121,10 +122,12 @@ class CharacterSheetStore(
private suspend fun updateCharacterSkillChange( private suspend fun updateCharacterSkillChange(
characterId: String, characterId: String,
skillId: String, skillId: String,
used: Boolean,
) { ) {
val character = useCase.updateSkillUsage( val character = useCase.updateSkillUsage(
character = characterDetail(characterId = characterId), character = characterDetail(characterId = characterId),
skillId = skillId, skillId = skillId,
used = used,
) )
_detailFlow.update(character) _detailFlow.update(character)
} }

View file

@ -11,9 +11,9 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
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.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Surface import androidx.compose.material.Surface
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@ -23,6 +23,7 @@ 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
import androidx.compose.ui.draw.clip
import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.key
@ -51,6 +52,7 @@ import org.koin.compose.viewmodel.koinViewModel
@Composable @Composable
fun CampaignScreen( fun CampaignScreen(
campaignViewModel: CampaignViewModel = koinViewModel(),
characterDetailViewModel: CharacterDetailViewModel = koinViewModel(), characterDetailViewModel: CharacterDetailViewModel = koinViewModel(),
characteristicDialogViewModel: CharacterDetailCharacteristicDialogViewModel = koinViewModel(), characteristicDialogViewModel: CharacterDetailCharacteristicDialogViewModel = koinViewModel(),
dismissedViewModel: CharacterDiminishedViewModel = koinViewModel(), dismissedViewModel: CharacterDiminishedViewModel = koinViewModel(),
@ -59,6 +61,7 @@ fun CampaignScreen(
) { ) {
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
networkViewModel.connect() networkViewModel.connect()
campaignViewModel.init()
} }
KeyHandler { KeyHandler {
@ -87,7 +90,7 @@ fun CampaignScreen(
top = { top = {
Surface( Surface(
modifier = Modifier modifier = Modifier
.height(32.dp) // .height(32.dp)
.fillMaxWidth(), .fillMaxWidth(),
elevation = 1.dp, elevation = 1.dp,
) { ) {
@ -97,7 +100,7 @@ fun CampaignScreen(
bottom = { bottom = {
Surface( Surface(
modifier = Modifier modifier = Modifier
.height(48.dp) // .height(48.dp)
.fillMaxWidth(), .fillMaxWidth(),
elevation = 1.dp, elevation = 1.dp,
) { ) {
@ -120,7 +123,8 @@ fun CampaignScreen(
modifier = Modifier modifier = Modifier
.width(width = 128.dp * 4) .width(width = 128.dp * 4)
.fillMaxHeight() .fillMaxHeight()
.padding(all = 8.dp), .padding(all = 8.dp)
.clip(shape = remember { RoundedCornerShape(16.dp) }),
blurController = blurController, blurController = blurController,
detailViewModel = characterDetailViewModel, detailViewModel = characterDetailViewModel,
rollViewModel = rollViewModel, rollViewModel = rollViewModel,
@ -180,9 +184,11 @@ fun CampaignScreen(
dismissedViewModel.changeDiminished( dismissedViewModel.changeDiminished(
dialog = diminished dialog = diminished
) )
blurController.hide()
dismissedViewModel.hideDiminishedDialog() dismissedViewModel.hideDiminishedDialog()
}, },
onDismissRequest = { onDismissRequest = {
blurController.hide()
dismissedViewModel.hideDiminishedDialog() dismissedViewModel.hideDiminishedDialog()
}, },
) )

View file

@ -0,0 +1,33 @@
package com.pixelized.desktop.lwa.ui.screen.campaign
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
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 kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
class CampaignViewModel(
private val characterRepository: CharacterSheetRepository,
private val alterationRepository: AlterationRepository,
private val campaignRepository: CampaignRepository,
) : ViewModel() {
suspend fun init() {
viewModelScope.launch {
campaignRepository.campaignFlow.collectLatest {
it.characters.keys.forEach { id ->
characterRepository.characterDetail(
characterSheetId = id.characterSheetId,
forceUpdate = true,
)
alterationRepository.updateActiveAlterations(
characterInstanceId = id,
)
}
}
}
}
}

View file

@ -8,6 +8,7 @@ import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.togetherWith import androidx.compose.animation.togetherWith
import androidx.compose.foundation.background
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.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@ -21,19 +22,16 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.composable.blur.BlurContentController 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.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.CharacterDetailHeader
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.header.CharacterDetailHeaderUio 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.CharacterDetailSheet
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.CharacterDetailSheetCharacteristicUio import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.CharacterDetailSheetCharacteristicUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.CharacterDetailSheetSkillUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.CharacterDetailSheetUio 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.screen.roll.RollViewModel
import com.pixelized.desktop.lwa.ui.theme.lwa import com.pixelized.desktop.lwa.ui.theme.lwa
@ -68,6 +66,7 @@ fun CharacterDetailPanel(
}, },
onDiminished = { onDiminished = {
scope.launch { scope.launch {
blurController.show()
characterDiminishedViewModel.showDiminishedDialog( characterDiminishedViewModel.showDiminishedDialog(
characterInstanceId = it characterInstanceId = it
) )
@ -92,14 +91,22 @@ fun CharacterDetailPanel(
} }
}, },
onCharacteristic = { onCharacteristic = {
rollViewModel.prepareRoll( rollViewModel.prepareRoll(roll = it.roll)
characterSheetId = detail.value.characterInstanceId?.characterSheetId!!,
label = it.label,
rollAction = "1d100",
rollSuccessValue = (it.value.toIntOrNull() ?: 0) * 5,
)
blurController.show()
rollViewModel.showOverlay() rollViewModel.showOverlay()
blurController.show()
},
onSkill = {
rollViewModel.prepareRoll(roll = it.roll)
rollViewModel.showOverlay()
blurController.show()
},
onUseSkill = {
scope.launch {
detailViewModel.onSkillUse(
skillId = it.skillId,
used = it.used,
)
}
}, },
) )
} }
@ -113,6 +120,8 @@ fun CharacterDetailAnimatedPanel(
onHp: (id: Campaign.CharacterInstance.Id) -> Unit, onHp: (id: Campaign.CharacterInstance.Id) -> Unit,
onPp: (id: Campaign.CharacterInstance.Id) -> Unit, onPp: (id: Campaign.CharacterInstance.Id) -> Unit,
onCharacteristic: (CharacterDetailSheetCharacteristicUio) -> Unit, onCharacteristic: (CharacterDetailSheetCharacteristicUio) -> Unit,
onSkill: (CharacterDetailSheetSkillUio) -> Unit,
onUseSkill: (CharacterDetailSheetSkillUio) -> Unit,
) { ) {
Box( Box(
modifier = modifier, modifier = modifier,
@ -148,6 +157,8 @@ fun CharacterDetailAnimatedPanel(
onHp = { onHp(it.characterInstanceId) }, onHp = { onHp(it.characterInstanceId) },
onPp = { onPp(it.characterInstanceId) }, onPp = { onPp(it.characterInstanceId) },
onCharacteristic = onCharacteristic, onCharacteristic = onCharacteristic,
onSkill = onSkill,
onUseSkill = onUseSkill,
) )
} }
} }
@ -166,14 +177,19 @@ fun CharacterDetailContent(
onHp: () -> Unit, onHp: () -> Unit,
onPp: () -> Unit, onPp: () -> Unit,
onCharacteristic: (CharacterDetailSheetCharacteristicUio) -> Unit, onCharacteristic: (CharacterDetailSheetCharacteristicUio) -> Unit,
onSkill: (CharacterDetailSheetSkillUio) -> Unit,
onUseSkill: (CharacterDetailSheetSkillUio) -> Unit,
) { ) {
Surface( Surface(
modifier = modifier.fillMaxSize(), modifier = modifier.fillMaxSize(),
color = MaterialTheme.lwa.colorScheme.elevatedSurface, color = MaterialTheme.lwa.colorScheme.elevated.base1dp,
) { ) {
Column { Column {
CharacterDetailHeader( CharacterDetailHeader(
modifier = Modifier.padding(start = 16.dp).fillMaxWidth(), modifier = Modifier
.background(color = MaterialTheme.lwa.colorScheme.elevated.base2dp)
.padding(bottom = 8.dp)
.fillMaxWidth(),
header = header, header = header,
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
onDiminished = onDiminished, onDiminished = onDiminished,
@ -184,9 +200,12 @@ fun CharacterDetailContent(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.verticalScroll(state = rememberScrollState()) .verticalScroll(state = rememberScrollState())
.padding(all = 16.dp), .padding(horizontal = 16.dp)
.padding(top = 8.dp, bottom = 16.dp),
sheet = sheet, sheet = sheet,
onCharacteristic = onCharacteristic, onCharacteristic = onCharacteristic,
onSkill = onSkill,
onUseSkill = onUseSkill,
) )
} }
} }

View file

@ -3,13 +3,16 @@ 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.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.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.CharacterDetailSheetCharacteristicUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.CharacterDetailSheetSkillUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.CharacterDetailSheetUio import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.CharacterDetailSheetUio
import com.pixelized.desktop.lwa.ui.screen.roll.RollActionUio
import com.pixelized.shared.lwa.model.AlteredCharacterSheetFactory import com.pixelized.shared.lwa.model.AlteredCharacterSheetFactory
import com.pixelized.shared.lwa.model.alteration.FieldAlteration import com.pixelized.shared.lwa.model.alteration.FieldAlteration
import com.pixelized.shared.lwa.model.campaign.Campaign import com.pixelized.shared.lwa.model.campaign.Campaign
import com.pixelized.shared.lwa.model.campaign.damage import com.pixelized.shared.lwa.model.campaign.damage
import com.pixelized.shared.lwa.model.campaign.power import com.pixelized.shared.lwa.model.campaign.power
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet
import com.pixelized.shared.lwa.usecase.ExpressionUseCase
import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.character_sheet__characteristics__cha 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__con
@ -26,9 +29,11 @@ import lwacharactersheet.composeapp.generated.resources.tooltip__characteristics
import lwacharactersheet.composeapp.generated.resources.tooltip__characteristics__power import lwacharactersheet.composeapp.generated.resources.tooltip__characteristics__power
import lwacharactersheet.composeapp.generated.resources.tooltip__characteristics__strength import lwacharactersheet.composeapp.generated.resources.tooltip__characteristics__strength
import org.jetbrains.compose.resources.getString import org.jetbrains.compose.resources.getString
import java.text.Collator
class CharacterDetailFactory( class CharacterDetailFactory(
private val alteredCharacterSheetFactory: AlteredCharacterSheetFactory, private val alteredCharacterSheetFactory: AlteredCharacterSheetFactory,
private val expressionUseCase: ExpressionUseCase,
) { ) {
fun convertToCharacterDetailHeaderUio( fun convertToCharacterDetailHeaderUio(
characterInstanceId: Campaign.CharacterInstance.Id, characterInstanceId: Campaign.CharacterInstance.Id,
@ -54,14 +59,17 @@ class CharacterDetailFactory(
maxHp = "$maxHp", maxHp = "$maxHp",
pp = "${maxPp - characterInstance.power}", pp = "${maxPp - characterInstance.power}",
maxPp = "$maxPp", maxPp = "$maxPp",
mov = "${alteredCharacterSheet.movement}" mov = "${alteredCharacterSheet.movement}",
armor = "${alteredCharacterSheet.armor}",
bonus = alteredCharacterSheet.damageBonus,
grow = "${alteredCharacterSheet.hpGrow}",
learn = "${alteredCharacterSheet.learning}",
) )
} }
suspend fun convertToCharacterDetailSheetUio( suspend fun convertToCharacterDetailSheetUio(
characterInstanceId: Campaign.CharacterInstance.Id, characterInstanceId: Campaign.CharacterInstance.Id,
characterSheet: CharacterSheet?, characterSheet: CharacterSheet?,
characterInstance: Campaign.CharacterInstance,
alterations: Map<String, List<FieldAlteration>>, alterations: Map<String, List<FieldAlteration>>,
): CharacterDetailSheetUio? { ): CharacterDetailSheetUio? {
if (characterSheet == null) return null if (characterSheet == null) return null
@ -81,6 +89,12 @@ class CharacterDetailFactory(
title = getString(Res.string.character_sheet__characteristics__str), title = getString(Res.string.character_sheet__characteristics__str),
description = getString(Res.string.tooltip__characteristics__strength), description = getString(Res.string.tooltip__characteristics__strength),
), ),
roll = RollActionUio(
characterSheetId = characterSheet.id,
label = getString(Res.string.character_sheet__characteristics__str),
rollAction = "1d100",
rollSuccessValue = alteredCharacterSheet.strength * 5,
),
), ),
CharacterDetailSheetCharacteristicUio( CharacterDetailSheetCharacteristicUio(
label = getString(Res.string.character_sheet__characteristics__dex), label = getString(Res.string.character_sheet__characteristics__dex),
@ -89,6 +103,12 @@ class CharacterDetailFactory(
title = getString(Res.string.character_sheet__characteristics__dex), title = getString(Res.string.character_sheet__characteristics__dex),
description = getString(Res.string.tooltip__characteristics__dexterity), description = getString(Res.string.tooltip__characteristics__dexterity),
), ),
roll = RollActionUio(
characterSheetId = characterSheet.id,
label = getString(Res.string.character_sheet__characteristics__dex),
rollAction = "1d100",
rollSuccessValue = alteredCharacterSheet.dexterity * 5,
),
), ),
CharacterDetailSheetCharacteristicUio( CharacterDetailSheetCharacteristicUio(
label = getString(Res.string.character_sheet__characteristics__con), label = getString(Res.string.character_sheet__characteristics__con),
@ -97,6 +117,12 @@ class CharacterDetailFactory(
title = getString(Res.string.character_sheet__characteristics__con), title = getString(Res.string.character_sheet__characteristics__con),
description = getString(Res.string.tooltip__characteristics__constitution), description = getString(Res.string.tooltip__characteristics__constitution),
), ),
roll = RollActionUio(
characterSheetId = characterSheet.id,
label = getString(Res.string.character_sheet__characteristics__con),
rollAction = "1d100",
rollSuccessValue = alteredCharacterSheet.constitution * 5,
),
), ),
CharacterDetailSheetCharacteristicUio( CharacterDetailSheetCharacteristicUio(
label = getString(Res.string.character_sheet__characteristics__hei), label = getString(Res.string.character_sheet__characteristics__hei),
@ -105,6 +131,12 @@ class CharacterDetailFactory(
title = getString(Res.string.character_sheet__characteristics__hei), title = getString(Res.string.character_sheet__characteristics__hei),
description = getString(Res.string.tooltip__characteristics__height), description = getString(Res.string.tooltip__characteristics__height),
), ),
roll = RollActionUio(
characterSheetId = characterSheet.id,
label = getString(Res.string.character_sheet__characteristics__hei),
rollAction = "1d100",
rollSuccessValue = alteredCharacterSheet.height * 5,
),
), ),
CharacterDetailSheetCharacteristicUio( CharacterDetailSheetCharacteristicUio(
label = getString(Res.string.character_sheet__characteristics__int), label = getString(Res.string.character_sheet__characteristics__int),
@ -113,6 +145,12 @@ class CharacterDetailFactory(
title = getString(Res.string.character_sheet__characteristics__int), title = getString(Res.string.character_sheet__characteristics__int),
description = getString(Res.string.tooltip__characteristics__intelligence), description = getString(Res.string.tooltip__characteristics__intelligence),
), ),
roll = RollActionUio(
characterSheetId = characterSheet.id,
label = getString(Res.string.character_sheet__characteristics__int),
rollAction = "1d100",
rollSuccessValue = alteredCharacterSheet.intelligence * 5,
),
), ),
CharacterDetailSheetCharacteristicUio( CharacterDetailSheetCharacteristicUio(
label = getString(Res.string.character_sheet__characteristics__pow), label = getString(Res.string.character_sheet__characteristics__pow),
@ -121,6 +159,12 @@ class CharacterDetailFactory(
title = getString(Res.string.character_sheet__characteristics__pow), title = getString(Res.string.character_sheet__characteristics__pow),
description = getString(Res.string.tooltip__characteristics__power), description = getString(Res.string.tooltip__characteristics__power),
), ),
roll = RollActionUio(
characterSheetId = characterSheet.id,
label = getString(Res.string.character_sheet__characteristics__pow),
rollAction = "1d100",
rollSuccessValue = alteredCharacterSheet.power * 5,
),
), ),
CharacterDetailSheetCharacteristicUio( CharacterDetailSheetCharacteristicUio(
label = getString(Res.string.character_sheet__characteristics__cha), label = getString(Res.string.character_sheet__characteristics__cha),
@ -129,8 +173,89 @@ class CharacterDetailFactory(
title = getString(Res.string.character_sheet__characteristics__cha), title = getString(Res.string.character_sheet__characteristics__cha),
description = getString(Res.string.tooltip__characteristics__charisma), description = getString(Res.string.tooltip__characteristics__charisma),
), ),
roll = RollActionUio(
characterSheetId = characterSheet.id,
label = getString(Res.string.character_sheet__characteristics__cha),
rollAction = "1d100",
rollSuccessValue = alteredCharacterSheet.charisma * 5,
),
),
),
commonSkills = characterSheet.commonSkills.map { skill ->
val value = expressionUseCase.computeSkillValue(
sheet = characterSheet,
skill = skill,
alterations = alterations,
)
CharacterDetailSheetSkillUio(
skillId = skill.id,
label = skill.label,
value = "$value",
used = skill.used,
tooltips = skill.description?.let {
TooltipUio(
title = skill.label,
description = it,
)
},
roll = RollActionUio(
characterSheetId = characterInstanceId.characterSheetId,
label = skill.label,
rollAction = "1d100",
rollSuccessValue = value,
), ),
) )
}.sortedWith(compareBy(Collator.getInstance()) { it.label }),
characterSheet.specialSkills.map { skill ->
val value = expressionUseCase.computeSkillValue(
sheet = characterSheet,
skill = skill,
alterations = alterations,
)
CharacterDetailSheetSkillUio(
skillId = skill.id,
label = skill.label,
value = "$value",
used = skill.used,
tooltips = skill.description?.let {
TooltipUio(
title = skill.label,
description = it,
)
},
roll = RollActionUio(
characterSheetId = characterInstanceId.characterSheetId,
label = skill.label,
rollAction = "1d100",
rollSuccessValue = value,
),
)
}.sortedWith(compareBy(Collator.getInstance()) { it.label }),
magicSkill = characterSheet.magicSkills.map { skill ->
val value = expressionUseCase.computeSkillValue(
sheet = characterSheet,
skill = skill,
alterations = alterations,
)
CharacterDetailSheetSkillUio(
skillId = skill.id,
label = skill.label,
value = "$value",
used = skill.used,
tooltips = skill.description?.let {
TooltipUio(
title = skill.label,
description = it,
)
},
roll = RollActionUio(
characterSheetId = characterInstanceId.characterSheetId,
label = skill.label,
rollAction = "1d100",
rollSuccessValue = value,
),
)
}.sortedWith(compareBy(Collator.getInstance()) { it.label }),
) )
} }
} }

View file

@ -5,7 +5,9 @@ import androidx.lifecycle.viewModelScope
import com.pixelized.desktop.lwa.repository.alteration.AlterationRepository import com.pixelized.desktop.lwa.repository.alteration.AlterationRepository
import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.desktop.lwa.repository.network.NetworkRepository
import com.pixelized.shared.lwa.model.campaign.Campaign import com.pixelized.shared.lwa.model.campaign.Campaign
import com.pixelized.shared.lwa.protocol.websocket.payload.UpdateSkillUsageMessage
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@ -18,6 +20,7 @@ class CharacterDetailViewModel(
private val campaignRepository: CampaignRepository, private val campaignRepository: CampaignRepository,
private val alterationRepository: AlterationRepository, private val alterationRepository: AlterationRepository,
private val characterDetailFactory: CharacterDetailFactory, private val characterDetailFactory: CharacterDetailFactory,
private val network: NetworkRepository,
) : ViewModel() { ) : ViewModel() {
private val displayedCharacterId = MutableStateFlow<Campaign.CharacterInstance.Id?>(null) private val displayedCharacterId = MutableStateFlow<Campaign.CharacterInstance.Id?>(null)
@ -25,7 +28,6 @@ class CharacterDetailViewModel(
val detail: StateFlow<CharacterDetailPanelUio> = displayedCharacterId val detail: StateFlow<CharacterDetailPanelUio> = displayedCharacterId
.map { characterInstanceId -> .map { characterInstanceId ->
if (characterInstanceId == null) return@map empty() if (characterInstanceId == null) return@map empty()
CharacterDetailPanelUio( CharacterDetailPanelUio(
characterInstanceId = characterInstanceId, characterInstanceId = characterInstanceId,
header = combine( header = combine(
@ -45,14 +47,12 @@ class CharacterDetailViewModel(
initialValue = null, initialValue = null,
), ),
sheet = combine( sheet = combine(
campaignRepository.characterInstanceFlow(id = characterInstanceId),
characterSheetRepository.characterDetailFlow(characterId = characterInstanceId.characterSheetId), characterSheetRepository.characterDetailFlow(characterId = characterInstanceId.characterSheetId),
alterationRepository.alterationsFlow(characterId = characterInstanceId), alterationRepository.alterationsFlow(characterId = characterInstanceId),
) { characterInstance, characterSheet, alterations -> ) { characterSheet, alterations ->
characterDetailFactory.convertToCharacterDetailSheetUio( characterDetailFactory.convertToCharacterDetailSheetUio(
characterInstanceId = characterInstanceId, characterInstanceId = characterInstanceId,
characterSheet = characterSheet, characterSheet = characterSheet,
characterInstance = characterInstance,
alterations = alterations, alterations = alterations,
) )
}.stateIn( }.stateIn(
@ -76,6 +76,20 @@ class CharacterDetailViewModel(
displayedCharacterId.value = null displayedCharacterId.value = null
} }
suspend fun onSkillUse(
skillId: String,
used: Boolean,
) {
val characterSheetId = displayedCharacterId.value?.characterSheetId ?: return
network.share(
payload = UpdateSkillUsageMessage(
characterSheetId = characterSheetId,
skillId = skillId,
used = used.not(),
)
)
}
private fun empty() = CharacterDetailPanelUio( private fun empty() = CharacterDetailPanelUio(
characterInstanceId = null, characterInstanceId = null,
header = MutableStateFlow(null), header = MutableStateFlow(null),

View file

@ -4,6 +4,7 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
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.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
@ -19,14 +20,19 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontWeight 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.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.theme.lwa import com.pixelized.desktop.lwa.ui.theme.lwa
import com.pixelized.shared.lwa.model.campaign.Campaign import com.pixelized.shared.lwa.model.campaign.Campaign
import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.ic_close_24dp import lwacharactersheet.composeapp.generated.resources.ic_close_24dp
import lwacharactersheet.composeapp.generated.resources.ic_cognition_24dp
import lwacharactersheet.composeapp.generated.resources.ic_heart_24dp import lwacharactersheet.composeapp.generated.resources.ic_heart_24dp
import lwacharactersheet.composeapp.generated.resources.ic_heart_plus_24dp
import lwacharactersheet.composeapp.generated.resources.ic_near_me import lwacharactersheet.composeapp.generated.resources.ic_near_me
import lwacharactersheet.composeapp.generated.resources.ic_shield_24dp
import lwacharactersheet.composeapp.generated.resources.ic_skull_24dp import lwacharactersheet.composeapp.generated.resources.ic_skull_24dp
import lwacharactersheet.composeapp.generated.resources.ic_swords_24dp
import lwacharactersheet.composeapp.generated.resources.ic_water_drop_24dp import lwacharactersheet.composeapp.generated.resources.ic_water_drop_24dp
import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.painterResource
@ -40,11 +46,16 @@ data class CharacterDetailHeaderUio(
val pp: String, val pp: String,
val maxPp: String, val maxPp: String,
val mov: String, val mov: String,
val armor: String,
val bonus: String,
val grow: String,
val learn: String,
) )
@Composable @Composable
fun CharacterDetailHeader( fun CharacterDetailHeader(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
iconSize: Dp = 14.dp,
header: State<CharacterDetailHeaderUio?>, header: State<CharacterDetailHeaderUio?>,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
onDiminished: () -> Unit, onDiminished: () -> Unit,
@ -54,7 +65,7 @@ fun CharacterDetailHeader(
Column( Column(
modifier = modifier, modifier = modifier,
) { ) {
Row { Row(modifier = Modifier.padding(start = 16.dp)) {
Text( Text(
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
.align(alignment = Alignment.CenterVertically), .align(alignment = Alignment.CenterVertically),
@ -84,14 +95,17 @@ fun CharacterDetailHeader(
} }
} }
Row( Row(
horizontalArrangement = Arrangement.spacedBy(space = 12.dp), modifier = Modifier.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(space = 16.dp),
) { ) {
Row( Row(
modifier = Modifier.clip(shape = CircleShape).clickable { onHp() }, modifier = Modifier.clip(shape = CircleShape).clickable { onHp() },
verticalAlignment = Alignment.Bottom, verticalAlignment = Alignment.Bottom,
) { ) {
Icon( Icon(
modifier = Modifier.padding(bottom = 4.dp, end = 2.dp).size(12.dp), modifier = Modifier
.padding(bottom = 4.dp, end = 2.dp)
.size(size = iconSize),
painter = painterResource(Res.drawable.ic_heart_24dp), painter = painterResource(Res.drawable.ic_heart_24dp),
contentDescription = null contentDescription = null
) )
@ -114,7 +128,9 @@ fun CharacterDetailHeader(
verticalAlignment = Alignment.Bottom, verticalAlignment = Alignment.Bottom,
) { ) {
Icon( Icon(
modifier = Modifier.padding(bottom = 4.dp, end = 2.dp).size(12.dp), modifier = Modifier
.padding(bottom = 4.dp, end = 2.dp)
.size(size = iconSize),
painter = painterResource(Res.drawable.ic_water_drop_24dp), painter = painterResource(Res.drawable.ic_water_drop_24dp),
contentDescription = null contentDescription = null
) )
@ -132,11 +148,16 @@ fun CharacterDetailHeader(
text = "/${header.value?.maxPp ?: ""}", text = "/${header.value?.maxPp ?: ""}",
) )
} }
Spacer(modifier = Modifier.weight(1f))
Row( Row(
verticalAlignment = Alignment.Bottom, verticalAlignment = Alignment.Bottom,
) { ) {
Icon( Icon(
modifier = Modifier.padding(bottom = 4.dp, end = 2.dp).size(12.dp), modifier = Modifier
.padding(bottom = 4.dp, end = 2.dp)
.size(size = iconSize),
painter = painterResource(Res.drawable.ic_near_me), painter = painterResource(Res.drawable.ic_near_me),
contentDescription = null, contentDescription = null,
) )
@ -151,6 +172,70 @@ fun CharacterDetailHeader(
text = "m", text = "m",
) )
} }
Row(
verticalAlignment = Alignment.Bottom,
) {
Icon(
modifier = Modifier
.padding(bottom = 4.dp, end = 2.dp)
.size(size = iconSize),
painter = painterResource(Res.drawable.ic_shield_24dp),
contentDescription = null,
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.h6,
text = header.value?.armor ?: "",
)
}
Row(
verticalAlignment = Alignment.Bottom,
) {
Icon(
modifier = Modifier
.padding(bottom = 4.dp, end = 2.dp)
.size(size = iconSize),
painter = painterResource(Res.drawable.ic_swords_24dp),
contentDescription = null,
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.h6,
text = header.value?.bonus ?: "",
)
}
Row(
verticalAlignment = Alignment.Bottom,
) {
Icon(
modifier = Modifier
.padding(bottom = 4.dp, end = 2.dp)
.size(size = iconSize),
painter = painterResource(Res.drawable.ic_heart_plus_24dp),
contentDescription = null,
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.h6,
text = header.value?.grow ?: "",
)
}
Row(
verticalAlignment = Alignment.Bottom,
) {
Icon(
modifier = Modifier
.padding(bottom = 4.dp, end = 2.dp)
.size(size = iconSize),
painter = painterResource(Res.drawable.ic_cognition_24dp),
contentDescription = null,
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.h6,
text = header.value?.learn ?: "",
)
}
} }
} }
} }

View file

@ -0,0 +1,97 @@
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.fillMaxWidth
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.desktop.lwa.ui.composable.decoratedBox.DecoratedBox
import com.pixelized.shared.lwa.model.campaign.Campaign
@Stable
data class CharacterDetailSheetUio(
val characterInstanceId: Campaign.CharacterInstance.Id,
val characteristics: List<CharacterDetailSheetCharacteristicUio>,
val commonSkills: List<CharacterDetailSheetSkillUio>,
val specialSkill: List<CharacterDetailSheetSkillUio>,
val magicSkill: List<CharacterDetailSheetSkillUio>,
)
@Composable
fun CharacterDetailSheet(
modifier: Modifier = Modifier,
sheet: State<CharacterDetailSheetUio?>,
onCharacteristic: (CharacterDetailSheetCharacteristicUio) -> Unit,
onSkill: (CharacterDetailSheetSkillUio) -> Unit,
onUseSkill: (CharacterDetailSheetSkillUio) -> Unit,
) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
) {
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(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(space = 8.dp)
) {
DecoratedBox(
modifier = Modifier.fillMaxWidth(),
) {
Column {
sheet.value?.commonSkills?.forEach { skill ->
CharacterDetailSheetSkill(
modifier = Modifier.fillMaxWidth(),
skill = skill,
onSkill = onSkill,
onUse = onUseSkill,
)
}
}
}
DecoratedBox(
modifier = Modifier.fillMaxWidth(),
) {
Column {
sheet.value?.specialSkill?.forEach { skill ->
CharacterDetailSheetSkill(
modifier = Modifier.fillMaxWidth(),
skill = skill,
onSkill = onSkill,
onUse = onUseSkill,
)
}
}
}
DecoratedBox(
modifier = Modifier.fillMaxWidth(),
) {
Column {
sheet.value?.magicSkill?.forEach { skill ->
CharacterDetailSheetSkill(
modifier = Modifier.fillMaxWidth(),
skill = skill,
onSkill = onSkill,
onUse = onUseSkill,
)
}
}
}
}
}
}

View file

@ -15,12 +15,14 @@ import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.composable.decoratedBox.DecoratedBox 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.TooltipLayout
import com.pixelized.desktop.lwa.ui.composable.tooltip.TooltipUio import com.pixelized.desktop.lwa.ui.composable.tooltip.TooltipUio
import com.pixelized.desktop.lwa.ui.screen.roll.RollActionUio
@Stable @Stable
data class CharacterDetailSheetCharacteristicUio( data class CharacterDetailSheetCharacteristicUio(
val value: String, val value: String,
val label: String, val label: String,
val tooltips: TooltipUio?, val tooltips: TooltipUio,
val roll: RollActionUio,
) )
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)

View file

@ -0,0 +1,79 @@
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.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.Checkbox
import androidx.compose.material.CheckboxDefaults
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 com.pixelized.desktop.lwa.ui.composable.tooltip.TooltipLayout
import com.pixelized.desktop.lwa.ui.composable.tooltip.TooltipUio
import com.pixelized.desktop.lwa.ui.screen.roll.RollActionUio
@Stable
data class CharacterDetailSheetSkillUio(
val skillId: String,
val label: String,
val value: String,
val used: Boolean,
val tooltips: TooltipUio?,
val roll: RollActionUio,
)
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun CharacterDetailSheetSkill(
modifier: Modifier = Modifier,
paddingValues: PaddingValues = PaddingValues(start = 8.dp),
skill: CharacterDetailSheetSkillUio,
onSkill: (CharacterDetailSheetSkillUio) -> Unit,
onUse: (CharacterDetailSheetSkillUio) -> Unit,
) {
TooltipLayout(
tooltip = skill.tooltips,
content = {
Row(
modifier = Modifier
.clickable(onClick = { onSkill(skill) })
.padding(paddingValues = paddingValues)
.then(other = modifier),
horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier.weight(1f),
style = MaterialTheme.typography.body1,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = skill.label
)
Text(
style = MaterialTheme.typography.body1,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colors.primary,
text = skill.value,
)
Checkbox(
modifier = Modifier.size(size = 32.dp),
checked = skill.used,
colors = CheckboxDefaults.colors(
checkedColor = MaterialTheme.colors.primary,
),
onCheckedChange = { onUse(skill) },
)
}
},
)
}

View file

@ -1,47 +0,0 @@
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

@ -1,7 +1,9 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon package com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@ -10,6 +12,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
@ -52,10 +55,11 @@ fun PlayerPortrait(
) { ) {
val colorScheme = MaterialTheme.lwa.colorScheme val colorScheme = MaterialTheme.lwa.colorScheme
DecoratedBox( Box (
modifier = modifier modifier = modifier
.size(size = size) .size(size = size)
.clip(shape = remember { RoundedCornerShape(8.dp) }) .clip(shape = remember { RoundedCornerShape(8.dp) })
.background(color = MaterialTheme.lwa.colorScheme.elevated.base1dp)
.clickable { onCharacter(character.id) }, .clickable { onCharacter(character.id) },
) { ) {
AsyncImage( AsyncImage(
@ -73,17 +77,17 @@ fun PlayerPortrait(
drawRect( drawRect(
brush = Brush.verticalGradient( brush = Brush.verticalGradient(
listOf( listOf(
colorScheme.elevatedSurface.copy(alpha = 0.0f), colorScheme.elevated.base1dp.copy(alpha = 0.0f),
colorScheme.elevatedSurface.copy(alpha = 0.0f), colorScheme.elevated.base1dp.copy(alpha = 0.0f),
colorScheme.elevatedSurface.copy(alpha = 0.0f), colorScheme.elevated.base1dp.copy(alpha = 0.0f),
colorScheme.elevatedSurface.copy(alpha = 0.5f), colorScheme.elevated.base1dp.copy(alpha = 0.5f),
colorScheme.elevatedSurface.copy(alpha = 0.8f), colorScheme.elevated.base1dp.copy(alpha = 0.8f),
) )
) )
) )
drawContent() drawContent()
} }
.padding(all = 2.dp), .padding(vertical = 2.dp, horizontal = 4.dp),
verticalArrangement = Arrangement.aligned(alignment = Alignment.Bottom), verticalArrangement = Arrangement.aligned(alignment = Alignment.Bottom),
) { ) {
Row( Row(

View file

@ -62,19 +62,6 @@ class PlayerRibbonViewModel(
private val rolls = hashMapOf<String, MutableState<PlayerPortraitRollUio?>>() private val rolls = hashMapOf<String, MutableState<PlayerPortraitRollUio?>>()
init {
viewModelScope.launch {
campaignRepository.campaignFlow.collectLatest {
it.characters.keys.forEach { id ->
characterRepository.characterDetail(
characterSheetId = id.characterSheetId,
forceUpdate = true,
)
}
}
}
}
@Composable @Composable
@Stable @Stable
fun roll(characterSheetId: String): State<PlayerPortraitRollUio?> { fun roll(characterSheetId: String): State<PlayerPortraitRollUio?> {

View file

@ -84,8 +84,9 @@ class CharacterSheetViewModel(
viewModelScope.launch { viewModelScope.launch {
network.share( network.share(
payload = UpdateSkillUsageMessage( payload = UpdateSkillUsageMessage(
characterId = argument.characterInstanceId.characterSheetId, characterSheetId = argument.characterInstanceId.characterSheetId,
skillId = skill.id, skillId = skill.id,
used = skill.used.not(),
) )
) )
} }

View file

@ -0,0 +1,11 @@
package com.pixelized.desktop.lwa.ui.screen.roll
import androidx.compose.runtime.Stable
@Stable
data class RollActionUio(
val characterSheetId: String,
val label: String,
val rollAction: String,
val rollSuccessValue: Int?,
)

View file

@ -97,6 +97,15 @@ class RollViewModel(
} }
fun prepareRoll( fun prepareRoll(
roll: RollActionUio,
) = prepareRoll(
characterSheetId = roll.characterSheetId,
label = roll.label,
rollAction = roll.rollAction,
rollSuccessValue = roll.rollSuccessValue,
)
private fun prepareRoll(
characterSheetId: String, characterSheetId: String,
label: String, label: String,
rollAction: String, rollAction: String,

View file

@ -14,21 +14,34 @@ import kotlin.math.ln
@Stable @Stable
data class LwaColorTheme( data class LwaColorTheme(
val base: Colors, val base: Colors,
val elevatedSurface: Color, val elevated: Elevated,
) ) {
@Stable
data class Elevated(
val base1dp: Color,
val base2dp: Color,
)
}
@Composable @Composable
@Stable @Stable
fun darkLwaColorTheme( fun darkLwaColorTheme(
base: Colors = darkColors(), base: Colors = darkColors(),
elevatedSurface: Color = base.calculateElevatedColor( elevated: LwaColorTheme.Elevated = LwaColorTheme.Elevated(
base1dp = base.calculateElevatedColor(
color = base.surface, color = base.surface,
onColor = base.onSurface, onColor = base.onSurface,
elevation = 1.dp, elevation = 1.dp,
), ),
base2dp = base.calculateElevatedColor(
color = base.surface,
onColor = base.onSurface,
elevation = 2.dp,
),
),
): LwaColorTheme = LwaColorTheme( ): LwaColorTheme = LwaColorTheme(
base = base, base = base,
elevatedSurface = elevatedSurface, elevated = elevated,
) )
@ReadOnlyComposable @ReadOnlyComposable

View file

@ -1,6 +1,5 @@
package com.pixelized.server.lwa.model.character package com.pixelized.server.lwa.model.character
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJson import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJson
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJsonFactory import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJsonFactory
import com.pixelized.shared.lwa.protocol.rest.CharacterPreviewJson import com.pixelized.shared.lwa.protocol.rest.CharacterPreviewJson
@ -39,7 +38,7 @@ class CharacterSheetService(
return store.save(sheet = factory.convertFromJson(character)) return store.save(sheet = factory.convertFromJson(character))
} }
fun deleteCharacter(characterId: String) : Boolean { fun deleteCharacter(characterId: String): Boolean {
return store.delete(id = characterId) return store.delete(id = characterId)
} }
@ -74,13 +73,15 @@ class CharacterSheetService(
} }
fun updateCharacterSkillUsage( fun updateCharacterSkillUsage(
characterId: String, characterSheetId: String,
skillId: String, skillId: String,
used: Boolean,
) { ) {
sheets[characterId]?.let { character -> sheets[characterSheetId]?.let { character ->
val update = useCase.updateSkillUsage( val update = useCase.updateSkillUsage(
character = character, character = character,
skillId = skillId, skillId = skillId,
used = used,
) )
store.save(sheet = update) store.save(sheet = update)
} }

View file

@ -33,8 +33,9 @@ class Engine(
} }
is UpdateSkillUsageMessage -> characterService.updateCharacterSkillUsage( is UpdateSkillUsageMessage -> characterService.updateCharacterSkillUsage(
characterId = data.characterId, characterSheetId = data.characterSheetId,
skillId = data.skillId skillId = data.skillId,
used = data.used,
) )
RestSynchronisation.Campaign -> Unit // Handle in the Rest RestSynchronisation.Campaign -> Unit // Handle in the Rest

View file

@ -4,6 +4,7 @@ import kotlinx.serialization.Serializable
@Serializable @Serializable
data class UpdateSkillUsageMessage( data class UpdateSkillUsageMessage(
val characterId: String, val characterSheetId: String,
val skillId: String, val skillId: String,
val used: Boolean,
) : MessagePayload ) : MessagePayload

View file

@ -89,16 +89,17 @@ class CharacterSheetUseCase {
fun updateSkillUsage( fun updateSkillUsage(
character: CharacterSheet, character: CharacterSheet,
skillId: String, skillId: String,
used: Boolean,
): CharacterSheet { ): CharacterSheet {
return character.copy( return character.copy(
commonSkills = character.commonSkills.map { skill -> commonSkills = character.commonSkills.map { skill ->
skill.takeIf { skill.id == skillId }?.copy(used = skill.used.not()) ?: skill skill.takeIf { skill.id == skillId }?.copy(used = used) ?: skill
}, },
specialSkills = character.specialSkills.map { skill -> specialSkills = character.specialSkills.map { skill ->
skill.takeIf { skill.id == skillId }?.copy(used = skill.used.not()) ?: skill skill.takeIf { skill.id == skillId }?.copy(used = used) ?: skill
}, },
magicSkills = character.magicSkills.map { skill -> magicSkills = character.magicSkills.map { skill ->
skill.takeIf { skill.id == skillId }?.copy(used = skill.used.not()) ?: skill skill.takeIf { skill.id == skillId }?.copy(used = used) ?: skill
}, },
) )
} }