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.ui.composable.character.characteristic.CharacterDetailCharacteristicDialogViewModel
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.CharacterDetailViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDiminishedViewModel
@ -106,6 +107,7 @@ val factoryDependencies
val viewModelDependencies
get() = module {
viewModelOf(::CampaignViewModel)
viewModelOf(::MainPageViewModel)
viewModelOf(::CharacterSheetViewModel)
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.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.Dispatchers
import kotlinx.coroutines.Job
@ -20,7 +20,7 @@ class AlterationRepository(
private val store: AlterationStore,
) {
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(
store.alterations,
store.active,
@ -38,17 +38,36 @@ class AlterationRepository(
)
fun alterationsFlow(
characterId: Campaign.CharacterInstance.Id,
characterId: CharacterInstance.Id,
): Flow<Map<String, List<FieldAlteration>>> {
return activeAlterationMapFlow.map { it[characterId] ?: emptyMap() }
}
fun alterations(
characterInstanceId: Campaign.CharacterInstance.Id,
characterInstanceId: CharacterInstance.Id,
): Map<String, List<FieldAlteration>> {
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(
alterations: Map<String, Alteration>,
actives: List<String>,
@ -71,15 +90,4 @@ class AlterationRepository(
}
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()
}
suspend fun updateActiveAlterations(
characterInstanceId: CharacterInstance.Id,
) {
_active.value = _active.value.toMutableMap().also {
it[characterInstanceId] = loadActiveAlterations(
characterInstanceId = characterInstanceId,
)
}
}
private suspend fun loadAlteration(): Map<String, Alteration> {
val request = client.alterations()
val data = request.map { alterationFactory.convertFromJson(json = it) }
@ -54,9 +64,6 @@ class AlterationStore(
characterSheetId = characterInstanceId.characterSheetId,
instanceId = characterInstanceId.instanceId,
)
_active.value = _active.value.toMutableMap().also {
it[characterInstanceId] = request
}
return request
}

View file

@ -109,8 +109,9 @@ class CharacterSheetStore(
is UpdateSkillUsageMessage -> {
updateCharacterSkillChange(
characterId = payload.characterId,
characterId = payload.characterSheetId,
skillId = payload.skillId,
used = payload.used,
)
}
@ -121,10 +122,12 @@ class CharacterSheetStore(
private suspend fun updateCharacterSkillChange(
characterId: String,
skillId: String,
used: Boolean,
) {
val character = useCase.updateSkillUsage(
character = characterDetail(characterId = characterId),
skillId = skillId,
used = used,
)
_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.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.foundation.shape.RoundedCornerShape
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@ -23,6 +23,7 @@ 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.input.key.Key
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key
@ -51,6 +52,7 @@ import org.koin.compose.viewmodel.koinViewModel
@Composable
fun CampaignScreen(
campaignViewModel: CampaignViewModel = koinViewModel(),
characterDetailViewModel: CharacterDetailViewModel = koinViewModel(),
characteristicDialogViewModel: CharacterDetailCharacteristicDialogViewModel = koinViewModel(),
dismissedViewModel: CharacterDiminishedViewModel = koinViewModel(),
@ -59,6 +61,7 @@ fun CampaignScreen(
) {
LaunchedEffect(Unit) {
networkViewModel.connect()
campaignViewModel.init()
}
KeyHandler {
@ -87,7 +90,7 @@ fun CampaignScreen(
top = {
Surface(
modifier = Modifier
.height(32.dp)
// .height(32.dp)
.fillMaxWidth(),
elevation = 1.dp,
) {
@ -97,7 +100,7 @@ fun CampaignScreen(
bottom = {
Surface(
modifier = Modifier
.height(48.dp)
// .height(48.dp)
.fillMaxWidth(),
elevation = 1.dp,
) {
@ -120,7 +123,8 @@ fun CampaignScreen(
modifier = Modifier
.width(width = 128.dp * 4)
.fillMaxHeight()
.padding(all = 8.dp),
.padding(all = 8.dp)
.clip(shape = remember { RoundedCornerShape(16.dp) }),
blurController = blurController,
detailViewModel = characterDetailViewModel,
rollViewModel = rollViewModel,
@ -180,9 +184,11 @@ fun CampaignScreen(
dismissedViewModel.changeDiminished(
dialog = diminished
)
blurController.hide()
dismissedViewModel.hideDiminishedDialog()
},
onDismissRequest = {
blurController.hide()
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.slideOutHorizontally
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
@ -21,19 +22,16 @@ 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.Modifier
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.CharacterDetailSheetSkillUio
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
@ -68,6 +66,7 @@ fun CharacterDetailPanel(
},
onDiminished = {
scope.launch {
blurController.show()
characterDiminishedViewModel.showDiminishedDialog(
characterInstanceId = it
)
@ -92,14 +91,22 @@ fun CharacterDetailPanel(
}
},
onCharacteristic = {
rollViewModel.prepareRoll(
characterSheetId = detail.value.characterInstanceId?.characterSheetId!!,
label = it.label,
rollAction = "1d100",
rollSuccessValue = (it.value.toIntOrNull() ?: 0) * 5,
)
blurController.show()
rollViewModel.prepareRoll(roll = it.roll)
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,
onPp: (id: Campaign.CharacterInstance.Id) -> Unit,
onCharacteristic: (CharacterDetailSheetCharacteristicUio) -> Unit,
onSkill: (CharacterDetailSheetSkillUio) -> Unit,
onUseSkill: (CharacterDetailSheetSkillUio) -> Unit,
) {
Box(
modifier = modifier,
@ -148,6 +157,8 @@ fun CharacterDetailAnimatedPanel(
onHp = { onHp(it.characterInstanceId) },
onPp = { onPp(it.characterInstanceId) },
onCharacteristic = onCharacteristic,
onSkill = onSkill,
onUseSkill = onUseSkill,
)
}
}
@ -166,14 +177,19 @@ fun CharacterDetailContent(
onHp: () -> Unit,
onPp: () -> Unit,
onCharacteristic: (CharacterDetailSheetCharacteristicUio) -> Unit,
onSkill: (CharacterDetailSheetSkillUio) -> Unit,
onUseSkill: (CharacterDetailSheetSkillUio) -> Unit,
) {
Surface(
modifier = modifier.fillMaxSize(),
color = MaterialTheme.lwa.colorScheme.elevatedSurface,
color = MaterialTheme.lwa.colorScheme.elevated.base1dp,
) {
Column {
CharacterDetailHeader(
modifier = Modifier.padding(start = 16.dp).fillMaxWidth(),
modifier = Modifier
.background(color = MaterialTheme.lwa.colorScheme.elevated.base2dp)
.padding(bottom = 8.dp)
.fillMaxWidth(),
header = header,
onDismissRequest = onDismissRequest,
onDiminished = onDiminished,
@ -184,9 +200,12 @@ fun CharacterDetailContent(
modifier = Modifier
.weight(1f)
.verticalScroll(state = rememberScrollState())
.padding(all = 16.dp),
.padding(horizontal = 16.dp)
.padding(top = 8.dp, bottom = 16.dp),
sheet = sheet,
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.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.CharacterDetailSheetSkillUio
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.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 com.pixelized.shared.lwa.usecase.ExpressionUseCase
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.character_sheet__characteristics__cha
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__strength
import org.jetbrains.compose.resources.getString
import java.text.Collator
class CharacterDetailFactory(
private val alteredCharacterSheetFactory: AlteredCharacterSheetFactory,
private val expressionUseCase: ExpressionUseCase,
) {
fun convertToCharacterDetailHeaderUio(
characterInstanceId: Campaign.CharacterInstance.Id,
@ -54,14 +59,17 @@ class CharacterDetailFactory(
maxHp = "$maxHp",
pp = "${maxPp - characterInstance.power}",
maxPp = "$maxPp",
mov = "${alteredCharacterSheet.movement}"
mov = "${alteredCharacterSheet.movement}",
armor = "${alteredCharacterSheet.armor}",
bonus = alteredCharacterSheet.damageBonus,
grow = "${alteredCharacterSheet.hpGrow}",
learn = "${alteredCharacterSheet.learning}",
)
}
suspend fun convertToCharacterDetailSheetUio(
characterInstanceId: Campaign.CharacterInstance.Id,
characterSheet: CharacterSheet?,
characterInstance: Campaign.CharacterInstance,
alterations: Map<String, List<FieldAlteration>>,
): CharacterDetailSheetUio? {
if (characterSheet == null) return null
@ -81,6 +89,12 @@ class CharacterDetailFactory(
title = getString(Res.string.character_sheet__characteristics__str),
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(
label = getString(Res.string.character_sheet__characteristics__dex),
@ -89,6 +103,12 @@ class CharacterDetailFactory(
title = getString(Res.string.character_sheet__characteristics__dex),
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(
label = getString(Res.string.character_sheet__characteristics__con),
@ -97,6 +117,12 @@ class CharacterDetailFactory(
title = getString(Res.string.character_sheet__characteristics__con),
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(
label = getString(Res.string.character_sheet__characteristics__hei),
@ -105,6 +131,12 @@ class CharacterDetailFactory(
title = getString(Res.string.character_sheet__characteristics__hei),
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(
label = getString(Res.string.character_sheet__characteristics__int),
@ -113,6 +145,12 @@ class CharacterDetailFactory(
title = getString(Res.string.character_sheet__characteristics__int),
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(
label = getString(Res.string.character_sheet__characteristics__pow),
@ -121,6 +159,12 @@ class CharacterDetailFactory(
title = getString(Res.string.character_sheet__characteristics__pow),
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(
label = getString(Res.string.character_sheet__characteristics__cha),
@ -129,8 +173,89 @@ class CharacterDetailFactory(
title = getString(Res.string.character_sheet__characteristics__cha),
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.campaign.CampaignRepository
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.protocol.websocket.payload.UpdateSkillUsageMessage
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
@ -18,6 +20,7 @@ class CharacterDetailViewModel(
private val campaignRepository: CampaignRepository,
private val alterationRepository: AlterationRepository,
private val characterDetailFactory: CharacterDetailFactory,
private val network: NetworkRepository,
) : ViewModel() {
private val displayedCharacterId = MutableStateFlow<Campaign.CharacterInstance.Id?>(null)
@ -25,7 +28,6 @@ class CharacterDetailViewModel(
val detail: StateFlow<CharacterDetailPanelUio> = displayedCharacterId
.map { characterInstanceId ->
if (characterInstanceId == null) return@map empty()
CharacterDetailPanelUio(
characterInstanceId = characterInstanceId,
header = combine(
@ -45,14 +47,12 @@ class CharacterDetailViewModel(
initialValue = null,
),
sheet = combine(
campaignRepository.characterInstanceFlow(id = characterInstanceId),
characterSheetRepository.characterDetailFlow(characterId = characterInstanceId.characterSheetId),
alterationRepository.alterationsFlow(characterId = characterInstanceId),
) { characterInstance, characterSheet, alterations ->
) { characterSheet, alterations ->
characterDetailFactory.convertToCharacterDetailSheetUio(
characterInstanceId = characterInstanceId,
characterSheet = characterSheet,
characterInstance = characterInstance,
alterations = alterations,
)
}.stateIn(
@ -76,6 +76,20 @@ class CharacterDetailViewModel(
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(
characterInstanceId = 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.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
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.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
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_cognition_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_shield_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 org.jetbrains.compose.resources.painterResource
@ -40,11 +46,16 @@ data class CharacterDetailHeaderUio(
val pp: String,
val maxPp: String,
val mov: String,
val armor: String,
val bonus: String,
val grow: String,
val learn: String,
)
@Composable
fun CharacterDetailHeader(
modifier: Modifier = Modifier,
iconSize: Dp = 14.dp,
header: State<CharacterDetailHeaderUio?>,
onDismissRequest: () -> Unit,
onDiminished: () -> Unit,
@ -54,7 +65,7 @@ fun CharacterDetailHeader(
Column(
modifier = modifier,
) {
Row {
Row(modifier = Modifier.padding(start = 16.dp)) {
Text(
modifier = Modifier.weight(1f)
.align(alignment = Alignment.CenterVertically),
@ -84,14 +95,17 @@ fun CharacterDetailHeader(
}
}
Row(
horizontalArrangement = Arrangement.spacedBy(space = 12.dp),
modifier = Modifier.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(space = 16.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),
modifier = Modifier
.padding(bottom = 4.dp, end = 2.dp)
.size(size = iconSize),
painter = painterResource(Res.drawable.ic_heart_24dp),
contentDescription = null
)
@ -114,7 +128,9 @@ fun CharacterDetailHeader(
verticalAlignment = Alignment.Bottom,
) {
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),
contentDescription = null
)
@ -132,11 +148,16 @@ fun CharacterDetailHeader(
text = "/${header.value?.maxPp ?: ""}",
)
}
Spacer(modifier = Modifier.weight(1f))
Row(
verticalAlignment = Alignment.Bottom,
) {
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),
contentDescription = null,
)
@ -151,6 +172,70 @@ fun CharacterDetailHeader(
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.tooltip.TooltipLayout
import com.pixelized.desktop.lwa.ui.composable.tooltip.TooltipUio
import com.pixelized.desktop.lwa.ui.screen.roll.RollActionUio
@Stable
data class CharacterDetailSheetCharacteristicUio(
val value: String,
val label: String,
val tooltips: TooltipUio?,
val tooltips: TooltipUio,
val roll: RollActionUio,
)
@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
import androidx.compose.foundation.background
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.fillMaxSize
@ -10,6 +12,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Icon
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
@ -52,10 +55,11 @@ fun PlayerPortrait(
) {
val colorScheme = MaterialTheme.lwa.colorScheme
DecoratedBox(
Box (
modifier = modifier
.size(size = size)
.clip(shape = remember { RoundedCornerShape(8.dp) })
.background(color = MaterialTheme.lwa.colorScheme.elevated.base1dp)
.clickable { onCharacter(character.id) },
) {
AsyncImage(
@ -73,17 +77,17 @@ fun PlayerPortrait(
drawRect(
brush = Brush.verticalGradient(
listOf(
colorScheme.elevatedSurface.copy(alpha = 0.0f),
colorScheme.elevatedSurface.copy(alpha = 0.0f),
colorScheme.elevatedSurface.copy(alpha = 0.0f),
colorScheme.elevatedSurface.copy(alpha = 0.5f),
colorScheme.elevatedSurface.copy(alpha = 0.8f),
colorScheme.elevated.base1dp.copy(alpha = 0.0f),
colorScheme.elevated.base1dp.copy(alpha = 0.0f),
colorScheme.elevated.base1dp.copy(alpha = 0.0f),
colorScheme.elevated.base1dp.copy(alpha = 0.5f),
colorScheme.elevated.base1dp.copy(alpha = 0.8f),
)
)
)
drawContent()
}
.padding(all = 2.dp),
.padding(vertical = 2.dp, horizontal = 4.dp),
verticalArrangement = Arrangement.aligned(alignment = Alignment.Bottom),
) {
Row(

View file

@ -62,19 +62,6 @@ class PlayerRibbonViewModel(
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
@Stable
fun roll(characterSheetId: String): State<PlayerPortraitRollUio?> {

View file

@ -84,8 +84,9 @@ class CharacterSheetViewModel(
viewModelScope.launch {
network.share(
payload = UpdateSkillUsageMessage(
characterId = argument.characterInstanceId.characterSheetId,
characterSheetId = argument.characterInstanceId.characterSheetId,
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(
roll: RollActionUio,
) = prepareRoll(
characterSheetId = roll.characterSheetId,
label = roll.label,
rollAction = roll.rollAction,
rollSuccessValue = roll.rollSuccessValue,
)
private fun prepareRoll(
characterSheetId: String,
label: String,
rollAction: String,

View file

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

View file

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

View file

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

View file

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

View file

@ -89,16 +89,17 @@ class CharacterSheetUseCase {
fun updateSkillUsage(
character: CharacterSheet,
skillId: String,
used: Boolean,
): CharacterSheet {
return character.copy(
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 ->
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 ->
skill.takeIf { skill.id == skillId }?.copy(used = skill.used.not()) ?: skill
skill.takeIf { skill.id == skillId }?.copy(used = used) ?: skill
},
)
}