Change the UI of damage & fatigue dialogs.

This commit is contained in:
Thomas Andres Gomez 2025-06-15 14:51:40 +02:00
parent 741bb7cf25
commit 894d8db493
15 changed files with 354 additions and 223 deletions

View file

@ -84,6 +84,7 @@
<string name="character_sheet_edit__sub_characteristics__max_hit_point">Points de vie maximum</string> <string name="character_sheet_edit__sub_characteristics__max_hit_point">Points de vie maximum</string>
<string name="character_sheet_edit__sub_characteristics__hit_point">Points de vie</string> <string name="character_sheet_edit__sub_characteristics__hit_point">Points de vie</string>
<string name="character_sheet_edit__sub_characteristics__max_power_point">Points de pouvoir maximum</string> <string name="character_sheet_edit__sub_characteristics__max_power_point">Points de pouvoir maximum</string>
<string name="character_sheet_edit__use_armor">Utiliser l'armure</string>
<string name="character_sheet_edit__sub_characteristics__power_point">Points de pouvoir</string> <string name="character_sheet_edit__sub_characteristics__power_point">Points de pouvoir</string>
<string name="character_sheet_edit__sub_characteristics__damage_bonus">Bonus aux dégats</string> <string name="character_sheet_edit__sub_characteristics__damage_bonus">Bonus aux dégats</string>
<string name="character_sheet_edit__sub_characteristics__armor">Armure</string> <string name="character_sheet_edit__sub_characteristics__armor">Armure</string>
@ -253,6 +254,7 @@
<string name="chat__diminished_change">%1$s passe à %2$d d'état diminuée</string> <string name="chat__diminished_change">%1$s passe à %2$d d'état diminuée</string>
<string name="chat__characteristic_change__hp_down">%1$s subit %2$d point(s) de dommage</string> <string name="chat__characteristic_change__hp_down">%1$s subit %2$d point(s) de dommage</string>
<string name="chat__characteristic_change__hp_down_armor">%1$s subit %2$d point(s) de dommage (armure : %3$d)</string>
<string name="chat__characteristic_change__hp_up">%1$s récupère %2$d point(s) de vie</string> <string name="chat__characteristic_change__hp_up">%1$s récupère %2$d point(s) de vie</string>
<string name="chat__characteristic_change__pp_down">%1$s utilise %2$d point(s) de pouvoir</string> <string name="chat__characteristic_change__pp_down">%1$s utilise %2$d point(s) de pouvoir</string>
<string name="chat__characteristic_change__pp_up">%1$s récupère %2$d point(s) de pouvoir</string> <string name="chat__characteristic_change__pp_up">%1$s récupère %2$d point(s) de pouvoir</string>

View file

@ -0,0 +1,58 @@
package com.pixelized.desktop.lwa.ui.composable.button
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import com.pixelized.desktop.lwa.ui.theme.lwa
import kotlinx.coroutines.flow.StateFlow
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.ic_remove_24dp
import org.jetbrains.compose.resources.painterResource
@Composable
fun SignButton(
modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource? = null,
enabled: Boolean = true,
add: StateFlow<Boolean>,
onClick: () -> Unit,
) {
IconButton(
onClick = onClick,
modifier = modifier,
enabled = enabled,
interactionSource = interactionSource,
) {
val rotation = animateFloatAsState(
animationSpec = spring(stiffness = Spring.StiffnessMediumLow),
targetValue = when (add.collectAsState().value) {
true -> 90f
else -> 0f
}
)
Icon(
modifier = Modifier.graphicsLayer {
this.rotationZ = rotation.value * 2f
},
painter = painterResource(Res.drawable.ic_remove_24dp),
tint = MaterialTheme.lwa.colorScheme.base.primary,
contentDescription = null
)
Icon(
modifier = Modifier.graphicsLayer {
this.rotationZ = rotation.value * 3f
},
painter = painterResource(Res.drawable.ic_remove_24dp),
tint = MaterialTheme.lwa.colorScheme.base.primary,
contentDescription = null
)
}
}

View file

@ -1,40 +1,43 @@
package com.pixelized.desktop.lwa.ui.composable.character.characteristic package com.pixelized.desktop.lwa.ui.composable.character.characteristic
import androidx.compose.foundation.clickable import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
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.material.TextButton import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
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.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.input.key.Key
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.input.key.key
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.input.key.type
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog import com.pixelized.desktop.lwa.ui.composable.button.SignButton
import com.pixelized.desktop.lwa.ui.composable.decoratedBox.DecoratedBox import com.pixelized.desktop.lwa.ui.composable.character.LwaDialog
import com.pixelized.desktop.lwa.utils.extention.onPreviewEscape import com.pixelized.desktop.lwa.ui.composable.checkbox.LwaCheckBox
import com.pixelized.desktop.lwa.ui.composable.checkbox.LwaCheckBoxUio
import com.pixelized.desktop.lwa.ui.composable.key.KeyHandler
import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextField
import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextFieldUio
import com.pixelized.desktop.lwa.ui.theme.lwa
import kotlinx.coroutines.flow.StateFlow
import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__use_armor
import lwacharactersheet.composeapp.generated.resources.dialog__cancel_action import lwacharactersheet.composeapp.generated.resources.dialog__cancel_action
import lwacharactersheet.composeapp.generated.resources.dialog__confirm_action import lwacharactersheet.composeapp.generated.resources.dialog__confirm_action
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
@ -44,9 +47,10 @@ data class CharacterSheetCharacteristicDialogUio(
val characterSheetId: String, val characterSheetId: String,
val characteristic: Characteristic, val characteristic: Characteristic,
val label: String, val label: String,
val value: () -> TextFieldValue, val add: StateFlow<Boolean>,
val onValueChange: (TextFieldValue) -> Unit, val value: LwaTextFieldUio,
val maxValue: String, val enableArmor: LwaCheckBoxUio?,
val enableConfirm: StateFlow<Boolean>,
) { ) {
@Stable @Stable
enum class Characteristic { enum class Characteristic {
@ -55,125 +59,131 @@ data class CharacterSheetCharacteristicDialogUio(
} }
} }
@Composable @Stable
fun CharacterSheetCharacteristicDialog( object CharacterSheetCharacteristicDialogDefault {
dialog: State<CharacterSheetCharacteristicDialogUio?>, @Stable
onConfirm: (CharacterSheetCharacteristicDialogUio) -> Unit, val paddings: PaddingValues =
onDismissRequest: () -> Unit, PaddingValues(top = 16.dp, start = 8.dp, end = 8.dp, bottom = 8.dp)
) {
dialog.value?.let { @Stable
Dialog( val spacings: DpSize = DpSize(width = 4.dp, height = 8.dp)
onDismissRequest = onDismissRequest,
content = {
CharacterSheetCharacteristicContent(
dialog = it,
onConfirm = onConfirm,
onDismissRequest = onDismissRequest,
)
}
)
}
} }
@Composable @Composable
private fun CharacterSheetCharacteristicContent( fun CharacterSheetCharacteristicDialog(
dialog: CharacterSheetCharacteristicDialogUio, dialog: State<CharacterSheetCharacteristicDialogUio?>,
paddings: PaddingValues = CharacterSheetCharacteristicDialogDefault.paddings,
spacings: DpSize = CharacterSheetCharacteristicDialogDefault.spacings,
onConfirm: (CharacterSheetCharacteristicDialogUio) -> Unit, onConfirm: (CharacterSheetCharacteristicDialogUio) -> Unit,
onSwapSign: (CharacterSheetCharacteristicDialogUio) -> Unit,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
) { ) {
val typography = MaterialTheme.typography LwaDialog(
val colors = MaterialTheme.colors state = dialog,
onDismissRequest = onDismissRequest,
onConfirm = { dialog.value?.let(onConfirm) }
) { state ->
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) { focusRequester.requestFocus() }
val focusRequester = remember { FocusRequester() } CharacterSheetCharacteristicDialogKeyHandler(
LaunchedEffect(Unit) { onSwap = { onSwapSign(state) },
focusRequester.requestFocus() )
}
Box( Column(
modifier = Modifier modifier = Modifier.padding(paddingValues = paddings),
.clickable( verticalArrangement = Arrangement.spacedBy(space = spacings.height),
interactionSource = remember { MutableInteractionSource() }, horizontalAlignment = Alignment.CenterHorizontally,
indication = null, ) {
onClick = onDismissRequest, Text(
style = MaterialTheme.typography.caption,
text = state.label,
) )
.onPreviewEscape(
escape = onDismissRequest, Row(
enter = { onConfirm(dialog) }, horizontalArrangement = Arrangement.spacedBy(space = spacings.width),
) verticalAlignment = Alignment.Bottom,
.fillMaxSize() ) {
.padding(all = 32.dp), SignButton(
contentAlignment = Alignment.Center, modifier = Modifier
) { .size(size = 56.dp)
DecoratedBox { .background(
Surface { color = MaterialTheme.lwa.colorScheme.elevated.base1dp,
Column( shape = MaterialTheme.shapes.small,
verticalArrangement = Arrangement.spacedBy(space = 8.dp), ),
horizontalAlignment = Alignment.CenterHorizontally, add = state.add,
onClick = { onSwapSign(state) },
)
LwaTextField(
modifier = Modifier
.focusRequester(focusRequester = focusRequester)
.width(192.dp),
field = state.value,
)
}
state.enableArmor?.let {
Row (
modifier = Modifier.align(alignment = Alignment.End),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(
space = spacings.width,
alignment = Alignment.End
),
) { ) {
Text( Text(
modifier = Modifier.padding(top = 16.dp, start = 24.dp, end = 24.dp),
style = MaterialTheme.typography.caption, style = MaterialTheme.typography.caption,
text = dialog.label, text = stringResource(Res.string.character_sheet_edit__use_armor),
)
LwaCheckBox(
field = it,
)
}
}
Row(
modifier = Modifier.align(alignment = Alignment.End),
horizontalArrangement = Arrangement.spacedBy(
space = spacings.width,
alignment = Alignment.End
)
) {
TextButton(
onClick = onDismissRequest,
) {
Text(
color = MaterialTheme.colors.primary.copy(alpha = .7f),
text = stringResource(Res.string.dialog__cancel_action)
)
}
TextButton(
enabled = state.enableConfirm.collectAsState().value,
onClick = { onConfirm(state) },
) {
Text(
text = stringResource(Res.string.dialog__confirm_action)
) )
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
BasicTextField(
modifier = Modifier
.focusRequester(focusRequester = focusRequester)
.alignByBaseline()
.width(width = 120.dp),
textStyle = remember {
typography.h5.copy(
color = colors.primary,
textAlign = TextAlign.End,
fontWeight = FontWeight.Bold
)
},
cursorBrush = SolidColor(MaterialTheme.colors.primary),
singleLine = true,
keyboardActions = KeyboardActions { onConfirm(dialog) },
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
value = dialog.value(),
onValueChange = dialog.onValueChange,
)
Text(
modifier = Modifier.alignByBaseline(),
text = "/",
)
Text(
modifier = Modifier.alignByBaseline(),
text = dialog.maxValue,
)
}
Row(
modifier = Modifier
.padding(bottom = 4.dp)
.padding(horizontal = 16.dp)
.align(alignment = Alignment.End),
horizontalArrangement = Arrangement.spacedBy(
space = 4.dp,
alignment = Alignment.End
)
) {
TextButton(
onClick = onDismissRequest,
) {
Text(
color = MaterialTheme.colors.primary.copy(alpha = .7f),
text = stringResource(Res.string.dialog__cancel_action)
)
}
TextButton(
onClick = { onConfirm(dialog) },
) {
Text(
text = stringResource(Res.string.dialog__confirm_action)
)
}
}
} }
} }
} }
} }
} }
@Composable
private fun CharacterSheetCharacteristicDialogKeyHandler(
onSwap: () -> Unit,
) {
KeyHandler {
if (it.type == KeyEventType.KeyDown) {
when (it.key) {
Key.AltLeft, Key.AltRight -> {
onSwap()
true
}
else -> false
}
} else {
false
}
}
}

View file

@ -1,15 +1,22 @@
package com.pixelized.desktop.lwa.ui.composable.character.characteristic package com.pixelized.desktop.lwa.ui.composable.character.characteristic
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import com.pixelized.desktop.lwa.repository.alteration.AlterationRepository import com.pixelized.desktop.lwa.repository.alteration.AlterationRepository
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.desktop.lwa.ui.composable.character.characteristic.CharacterSheetCharacteristicDialogUio.Characteristic import com.pixelized.desktop.lwa.ui.composable.character.characteristic.CharacterSheetCharacteristicDialogUio.Characteristic
import com.pixelized.desktop.lwa.ui.composable.checkbox.createLwaCheckBox
import com.pixelized.desktop.lwa.ui.composable.checkbox.createLwaCheckBoxFlow
import com.pixelized.desktop.lwa.ui.composable.textfield.createLwaTextField
import com.pixelized.desktop.lwa.ui.composable.textfield.createLwaTextFieldFlow
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.characterSheet.CharacterSheet import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.character__inventory__add_to_purse__title
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__sub_characteristics__hit_point import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__sub_characteristics__hit_point
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__sub_characteristics__power_point import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__sub_characteristics__power_point
import org.jetbrains.compose.resources.getString import org.jetbrains.compose.resources.getString
@ -19,9 +26,13 @@ class CharacterSheetCharacteristicDialogFactory(
private val alterationRepository: AlterationRepository, private val alterationRepository: AlterationRepository,
private val alteredCharacterSheetFactory: AlteredCharacterSheetFactory, private val alteredCharacterSheetFactory: AlteredCharacterSheetFactory,
) { ) {
private val decimalChecker = Regex("""^\s*\d*\s*${'$'}""")
suspend fun convertToDialogUio( suspend fun convertToDialogUio(
scope: CoroutineScope,
characterSheetId: String?, characterSheetId: String?,
characteristic: Characteristic, characteristic: Characteristic,
signFlow: StateFlow<Boolean>,
): CharacterSheetCharacteristicDialogUio? { ): CharacterSheetCharacteristicDialogUio? {
if (characterSheetId == null) return null if (characterSheetId == null) return null
@ -32,10 +43,10 @@ class CharacterSheetCharacteristicDialogFactory(
if (characterSheet == null) return null if (characterSheet == null) return null
val alterations: Map<String, List<FieldAlteration>> = alterationRepository.activeFieldAlterations( val alterations: Map<String, List<FieldAlteration>> =
characterSheetId = characterSheetId, alterationRepository.activeFieldAlterations(
) characterSheetId = characterSheetId,
)
val alteredCharacterSheet = alteredCharacterSheetFactory.sheet( val alteredCharacterSheet = alteredCharacterSheetFactory.sheet(
characterSheet = characterSheet, characterSheet = characterSheet,
alterations = alterations, alterations = alterations,
@ -43,36 +54,61 @@ class CharacterSheetCharacteristicDialogFactory(
return when (characteristic) { return when (characteristic) {
Characteristic.Damage -> { Characteristic.Damage -> {
val value = mutableStateOf( val valueFlow = createLwaTextFieldFlow(
"${alteredCharacterSheet.maxHp - alteredCharacterSheet.damage}".let { label = null,
TextFieldValue(text = it, selection = TextRange(it.length))
}
) )
val armor = if (alteredCharacterSheet.armor > 0) {
createLwaCheckBoxFlow(
checked = true,
)
} else {
null
}
CharacterSheetCharacteristicDialogUio( CharacterSheetCharacteristicDialogUio(
characterSheetId = characterSheetId, characterSheetId = characterSheetId,
characteristic = characteristic, characteristic = characteristic,
label = getString(resource = Res.string.character_sheet_edit__sub_characteristics__hit_point), label = getString(Res.string.character_sheet_edit__sub_characteristics__hit_point).let {
value = { value.value }, "$it : ${alteredCharacterSheet.maxHp - alteredCharacterSheet.damage}/${alteredCharacterSheet.maxHp}"
onValueChange = { value.value = it }, },
maxValue = "${alteredCharacterSheet.maxHp}", add = signFlow,
value = valueFlow.createLwaTextField {
valueFlow.errorFlow.value = check(it)
valueFlow.valueFlow.value = it
},
enableArmor = armor?.createLwaCheckBox(),
enableConfirm = valueFlow.errorFlow.map { it.not() }.stateIn(
scope = scope,
started = SharingStarted.Lazily,
initialValue = valueFlow.errorFlow.value.not(),
),
) )
} }
Characteristic.Fatigue -> { Characteristic.Fatigue -> {
val value = mutableStateOf( val valueFlow = createLwaTextFieldFlow(
"${alteredCharacterSheet.maxPp - alteredCharacterSheet.fatigue}".let { label = null,
TextFieldValue(text = it, selection = TextRange(it.length))
}
) )
CharacterSheetCharacteristicDialogUio( CharacterSheetCharacteristicDialogUio(
characterSheetId = characterSheetId, characterSheetId = characterSheetId,
characteristic = characteristic, characteristic = characteristic,
label = getString(resource = Res.string.character_sheet_edit__sub_characteristics__power_point), label = getString(Res.string.character_sheet_edit__sub_characteristics__power_point).let {
value = { value.value }, "$it : ${alteredCharacterSheet.maxPp - alteredCharacterSheet.fatigue}/${alteredCharacterSheet.maxPp}"
onValueChange = { value.value = it }, },
maxValue = "${alteredCharacterSheet.maxPp}", add = signFlow,
value = valueFlow.createLwaTextField {
valueFlow.errorFlow.value = check(it)
valueFlow.valueFlow.value = it
},
enableArmor = null,
enableConfirm = valueFlow.errorFlow.map { it.not() }.stateIn(
scope = scope,
started = SharingStarted.Lazily,
initialValue = valueFlow.errorFlow.value.not(),
),
) )
} }
} }
} }
private fun check(value: String): Boolean = !decimalChecker.matches(value)
} }

View file

@ -3,12 +3,15 @@ package com.pixelized.desktop.lwa.ui.composable.character.characteristic
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
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.characterSheet.CharacterSheetRepository import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.desktop.lwa.repository.network.NetworkRepository import com.pixelized.desktop.lwa.repository.network.NetworkRepository
import com.pixelized.desktop.lwa.ui.composable.character.characteristic.CharacterSheetCharacteristicDialogUio.Characteristic import com.pixelized.desktop.lwa.ui.composable.character.characteristic.CharacterSheetCharacteristicDialogUio.Characteristic
import com.pixelized.shared.lwa.model.AlteredCharacterSheetFactory import com.pixelized.shared.lwa.model.AlteredCharacterSheetFactory
import com.pixelized.shared.lwa.protocol.websocket.CharacterSheetEvent import com.pixelized.shared.lwa.protocol.websocket.CharacterSheetEvent
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
class CharacterSheetCharacteristicDialogViewModel( class CharacterSheetCharacteristicDialogViewModel(
private val characterSheetRepository: CharacterSheetRepository, private val characterSheetRepository: CharacterSheetRepository,
@ -18,6 +21,8 @@ class CharacterSheetCharacteristicDialogViewModel(
private val network: NetworkRepository, private val network: NetworkRepository,
) : ViewModel() { ) : ViewModel() {
private val signFlow = MutableStateFlow(false)
private val _statChangeDialog = mutableStateOf<CharacterSheetCharacteristicDialogUio?>(null) private val _statChangeDialog = mutableStateOf<CharacterSheetCharacteristicDialogUio?>(null)
val statChangeDialog: State<CharacterSheetCharacteristicDialogUio?> get() = _statChangeDialog val statChangeDialog: State<CharacterSheetCharacteristicDialogUio?> get() = _statChangeDialog
@ -25,19 +30,27 @@ class CharacterSheetCharacteristicDialogViewModel(
_statChangeDialog.value = null _statChangeDialog.value = null
} }
fun swapCharacteristicSign() {
signFlow.update { it.not() }
}
suspend fun showSubCharacteristicDialog( suspend fun showSubCharacteristicDialog(
characterSheetId: String?, characterSheetId: String?,
characteristic: Characteristic, characteristic: Characteristic,
) { ) {
signFlow.update { false }
_statChangeDialog.value = factory.convertToDialogUio( _statChangeDialog.value = factory.convertToDialogUio(
scope = viewModelScope,
characterSheetId = characterSheetId, characterSheetId = characterSheetId,
characteristic = characteristic, characteristic = characteristic,
signFlow = signFlow,
) )
} }
suspend fun changeSubCharacteristic( suspend fun changeSubCharacteristic(
characterSheetId: String?, characterSheetId: String?,
characteristic: Characteristic, characteristic: Characteristic,
useArmor: Boolean,
value: Int, value: Int,
) { ) {
if (characterSheetId == null) return if (characterSheetId == null) return
@ -49,29 +62,37 @@ class CharacterSheetCharacteristicDialogViewModel(
if (characterSheet == null) return if (characterSheet == null) return
val alterations = alterationRepository.activeFieldAlterations( val sign = if (signFlow.value) 1 else -1
characterSheetId = characterSheetId,
)
// we need the maximum HP / Power that the character sheet have.
val alteredSheet = alteredCharacterSheetFactory.sheet(
characterSheet = characterSheet,
alterations = alterations,
)
val message = when (characteristic) { val message = when (characteristic) {
Characteristic.Damage -> CharacterSheetEvent.UpdateDamage( Characteristic.Damage -> {
timestamp = System.currentTimeMillis(), val armor = if (useArmor && signFlow.value.not()) {
characterSheetId = characterSheetId, val alterations = alterationRepository.activeFieldAlterations(
oldValue = alteredSheet.damage, characterSheetId = characterSheetId,
damage = alteredSheet.maxHp - value, )
) val alteredSheet = alteredCharacterSheetFactory.sheet(
characterSheet = characterSheet,
alterations = alterations,
)
alteredSheet.armor
} else {
null
}
CharacterSheetEvent.UpdateDamage(
timestamp = System.currentTimeMillis(),
characterSheetId = characterSheetId,
oldValue = characterSheet.damage,
damage = characterSheet.damage - value * sign - (armor ?: 0),
armor = armor,
)
}
Characteristic.Fatigue -> CharacterSheetEvent.UpdateFatigue( Characteristic.Fatigue -> CharacterSheetEvent.UpdateFatigue(
timestamp = System.currentTimeMillis(), timestamp = System.currentTimeMillis(),
characterSheetId = characterSheetId, characterSheetId = characterSheetId,
oldValue = alteredSheet.fatigue, oldValue = characterSheet.fatigue,
fatigue = alteredSheet.maxPp - value, fatigue = characterSheet.fatigue - value * sign,
) )
} }

View file

@ -1,9 +1,6 @@
package com.pixelized.desktop.lwa.ui.composable.character.purse package com.pixelized.desktop.lwa.ui.composable.character.purse
import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith import androidx.compose.animation.togetherWith
@ -19,8 +16,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface import androidx.compose.material.Surface
import androidx.compose.material.Text import androidx.compose.material.Text
@ -35,13 +30,13 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.graphicsLayer
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
import androidx.compose.ui.input.key.type import androidx.compose.ui.input.key.type
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.composable.button.SignButton
import com.pixelized.desktop.lwa.ui.composable.character.LwaDialog import com.pixelized.desktop.lwa.ui.composable.character.LwaDialog
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.key.KeyHandler import com.pixelized.desktop.lwa.ui.composable.key.KeyHandler
@ -53,8 +48,6 @@ import kotlinx.coroutines.flow.StateFlow
import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.dialog__cancel_action import lwacharactersheet.composeapp.generated.resources.dialog__cancel_action
import lwacharactersheet.composeapp.generated.resources.dialog__confirm_action import lwacharactersheet.composeapp.generated.resources.dialog__confirm_action
import lwacharactersheet.composeapp.generated.resources.ic_remove_24dp
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
@Stable @Stable
@ -94,6 +87,10 @@ fun PurseDialog(
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) { focusRequester.requestFocus() } LaunchedEffect(Unit) { focusRequester.requestFocus() }
PurseDialogKeyHandler(
onSwap = { onSwapSign(state) },
)
Column( Column(
modifier = Modifier.padding(paddingValues = paddings), modifier = Modifier.padding(paddingValues = paddings),
verticalArrangement = Arrangement.spacedBy(space = spacings.height), verticalArrangement = Arrangement.spacedBy(space = spacings.height),
@ -162,10 +159,6 @@ fun PurseDialog(
} }
} }
} }
PurseDialogKeyHandler(
onSwap = { onSwapSign(state) },
)
} }
} }
@ -202,46 +195,6 @@ private fun PurseContent(
} }
} }
@Composable
private fun SignButton(
modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource? = null,
enabled: Boolean = true,
add: StateFlow<Boolean>,
onClick: () -> Unit,
) {
IconButton(
onClick = onClick,
modifier = modifier,
enabled = enabled,
interactionSource = interactionSource,
) {
val rotation = animateFloatAsState(
animationSpec = spring(stiffness = Spring.StiffnessMediumLow),
targetValue = when (add.collectAsState().value) {
true -> 90f
else -> 0f
}
)
Icon(
modifier = Modifier.graphicsLayer {
this.rotationZ = rotation.value * 2f
},
painter = painterResource(Res.drawable.ic_remove_24dp),
tint = MaterialTheme.lwa.colorScheme.base.primary,
contentDescription = null
)
Icon(
modifier = Modifier.graphicsLayer {
this.rotationZ = rotation.value * 3f
},
painter = painterResource(Res.drawable.ic_remove_24dp),
tint = MaterialTheme.lwa.colorScheme.base.primary,
contentDescription = null
)
}
}
@Composable @Composable
private fun PurseDialogKeyHandler( private fun PurseDialogKeyHandler(
onSwap: () -> Unit, onSwap: () -> Unit,

View file

@ -0,0 +1,30 @@
package com.pixelized.desktop.lwa.ui.composable.checkbox
import androidx.compose.runtime.Stable
import kotlinx.coroutines.flow.MutableStateFlow
@Stable
data class LwaCheckBoxFlow(
val checkedFlow: MutableStateFlow<Boolean>,
)
@Stable
fun createLwaCheckBoxFlow(
checked: Boolean,
): LwaCheckBoxFlow {
return LwaCheckBoxFlow(
checkedFlow = MutableStateFlow(checked),
)
}
@Stable
fun LwaCheckBoxFlow.createLwaCheckBox(
enable: Boolean = true,
onCheckedChange: (Boolean) -> Unit = { checkedFlow.value = it },
): LwaCheckBoxUio {
return LwaCheckBoxUio(
enable = enable,
checked = checkedFlow,
onCheckedChange = onCheckedChange,
)
}

View file

@ -6,13 +6,13 @@ import kotlinx.coroutines.flow.MutableStateFlow
@Stable @Stable
data class LwaTextFieldFlow( data class LwaTextFieldFlow(
val errorFlow: MutableStateFlow<Boolean>, val errorFlow: MutableStateFlow<Boolean>,
val valueFlow: MutableStateFlow<String>,
val labelFlow: MutableStateFlow<String?>, val labelFlow: MutableStateFlow<String?>,
val valueFlow: MutableStateFlow<String>,
) )
fun createLwaTextFieldFlow( fun createLwaTextFieldFlow(
error: Boolean = false, error: Boolean = false,
label: String, label: String?,
value: String = "", value: String = "",
): LwaTextFieldFlow { ): LwaTextFieldFlow {
return createLwaTextFieldFlow( return createLwaTextFieldFlow(
@ -24,8 +24,8 @@ fun createLwaTextFieldFlow(
fun createLwaTextFieldFlow( fun createLwaTextFieldFlow(
errorFlow: MutableStateFlow<Boolean> = MutableStateFlow(false), errorFlow: MutableStateFlow<Boolean> = MutableStateFlow(false),
valueFlow: MutableStateFlow<String>,
labelFlow: MutableStateFlow<String?>, labelFlow: MutableStateFlow<String?>,
valueFlow: MutableStateFlow<String>,
): LwaTextFieldFlow { ): LwaTextFieldFlow {
return LwaTextFieldFlow( return LwaTextFieldFlow(
errorFlow = errorFlow, errorFlow = errorFlow,

View file

@ -206,12 +206,16 @@ fun CampaignScreen(
characteristicDialogViewModel.changeSubCharacteristic( characteristicDialogViewModel.changeSubCharacteristic(
characterSheetId = dialog.characterSheetId, characterSheetId = dialog.characterSheetId,
characteristic = dialog.characteristic, characteristic = dialog.characteristic,
value = dialog.value().text.toIntOrNull() ?: 0, useArmor= dialog.enableArmor?.checked?.value == true,
value = dialog.value.valueFlow.value.toIntOrNull() ?: 0,
) )
characteristicDialogViewModel.hideSubCharacteristicDialog() characteristicDialogViewModel.hideSubCharacteristicDialog()
blurController.hide() blurController.hide()
} }
}, },
onSwapSign = {
characteristicDialogViewModel.swapCharacteristicSign()
},
onDismissRequest = { onDismissRequest = {
characteristicDialogViewModel.hideSubCharacteristicDialog() characteristicDialogViewModel.hideSubCharacteristicDialog()
blurController.hide() blurController.hide()

View file

@ -17,6 +17,7 @@ import com.pixelized.shared.lwa.protocol.websocket.RollEvent
import com.pixelized.shared.lwa.protocol.websocket.SocketMessage import com.pixelized.shared.lwa.protocol.websocket.SocketMessage
import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.chat__characteristic_change__hp_down import lwacharactersheet.composeapp.generated.resources.chat__characteristic_change__hp_down
import lwacharactersheet.composeapp.generated.resources.chat__characteristic_change__hp_down_armor
import lwacharactersheet.composeapp.generated.resources.chat__characteristic_change__hp_up import lwacharactersheet.composeapp.generated.resources.chat__characteristic_change__hp_up
import lwacharactersheet.composeapp.generated.resources.chat__characteristic_change__pp_down import lwacharactersheet.composeapp.generated.resources.chat__characteristic_change__pp_down
import lwacharactersheet.composeapp.generated.resources.chat__characteristic_change__pp_up import lwacharactersheet.composeapp.generated.resources.chat__characteristic_change__pp_up
@ -94,16 +95,19 @@ class TextMessageFactory(
?: return null ?: return null
// We are talking about damage / consumption there so old value have to be put first. // We are talking about damage / consumption there so old value have to be put first.
val value = message.oldValue - message.damage val value = message.oldValue - message.damage
val armor = message.armor
CharacteristicTextMessageUio( CharacteristicTextMessageUio(
id = "${message.timestamp}-${message.characterSheetId}-Damage", id = "${message.timestamp}-${message.characterSheetId}-Damage",
timestamp = formatTime.format(message.timestamp), timestamp = formatTime.format(message.timestamp),
label = when { label = when {
value < 0 -> Res.string.chat__characteristic_change__hp_down value >= 0 -> Res.string.chat__characteristic_change__hp_up
else -> Res.string.chat__characteristic_change__hp_up armor != null -> Res.string.chat__characteristic_change__hp_down_armor
else -> Res.string.chat__characteristic_change__hp_down
}, },
character = sheet.name, character = sheet.name,
value = abs(value), value = abs(value),
armor = message.armor,
) )
} }
@ -127,6 +131,7 @@ class TextMessageFactory(
}, },
character = sheet.name, character = sheet.name,
value = abs(value), value = abs(value),
armor = 0,
) )
} }

View file

@ -21,6 +21,7 @@ data class CharacteristicTextMessageUio(
val label: StringResource, val label: StringResource,
val character: String, val character: String,
val value: Int, val value: Int,
val armor: Int?,
) : TextMessage ) : TextMessage
@Composable @Composable
@ -52,7 +53,7 @@ fun CharacteristicTextMessage(
style = MaterialTheme.lwa.typography.chat.text, style = MaterialTheme.lwa.typography.chat.text,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
maxLines = 1, maxLines = 1,
text = stringResource(message.label, message.character, message.value), text = stringResource(message.label, message.character, message.value, message.armor ?: 0),
) )
} }
} }

View file

@ -126,12 +126,16 @@ fun GMCharacterPage(
characteristicDialogViewModel.changeSubCharacteristic( characteristicDialogViewModel.changeSubCharacteristic(
characterSheetId = dialog.characterSheetId, characterSheetId = dialog.characterSheetId,
characteristic = dialog.characteristic, characteristic = dialog.characteristic,
value = dialog.value().text.toIntOrNull() ?: 0, useArmor= dialog.enableArmor?.checked?.value == true,
value = dialog.value.valueFlow.value.toIntOrNull() ?: 0,
) )
characteristicDialogViewModel.hideSubCharacteristicDialog() characteristicDialogViewModel.hideSubCharacteristicDialog()
blurController.hide() blurController.hide()
} }
}, },
onSwapSign = {
characteristicDialogViewModel.swapCharacteristicSign()
},
onDismissRequest = { onDismissRequest = {
characteristicDialogViewModel.hideSubCharacteristicDialog() characteristicDialogViewModel.hideSubCharacteristicDialog()
blurController.hide() blurController.hide()

View file

@ -18,6 +18,10 @@ fun Engine.putCharacterDamage(): suspend RoutingContext.() -> Unit {
name = "damage", name = "damage",
code = APIResponse.ErrorCode.Damage, code = APIResponse.ErrorCode.Damage,
) )
val armor: Int = call.queryParameters.param(
name = "armor",
code = APIResponse.ErrorCode.Armor,
)
// fetch the character sheet // fetch the character sheet
val characterSheet = characterService.character(characterSheetId) val characterSheet = characterService.character(characterSheetId)
?: error("CharacterSheet with id:$characterSheetId not found.") ?: error("CharacterSheet with id:$characterSheetId not found.")
@ -36,6 +40,7 @@ fun Engine.putCharacterDamage(): suspend RoutingContext.() -> Unit {
characterSheetId = characterSheetId, characterSheetId = characterSheetId,
oldValue = characterSheet.damage, oldValue = characterSheet.damage,
damage = damage, damage = damage,
armor = armor,
) )
) )
} catch (exception: Exception) { } catch (exception: Exception) {

View file

@ -22,6 +22,7 @@ data class APIResponse<T>(
Active, Active,
Equip, Equip,
Damage, Damage,
Armor,
Fatigue, Fatigue,
Diminished, Diminished,
Count, Count,

View file

@ -9,6 +9,7 @@ sealed interface CharacterSheetEvent : SocketMessage, CharacterSheetIdMessage {
data class UpdateDamage( data class UpdateDamage(
override val characterSheetId: String, override val characterSheetId: String,
override val timestamp: Long, override val timestamp: Long,
val armor: Int?,
val oldValue: Int, val oldValue: Int,
val damage: Int, val damage: Int,
) : CharacterSheetEvent ) : CharacterSheetEvent