Update client to add throw consume equip to InventoryItem.

This commit is contained in:
Thomas Andres Gomez 2025-04-19 21:46:03 +02:00
parent 9fce3f1cb8
commit ae820f5979
22 changed files with 791 additions and 121 deletions

View file

@ -184,8 +184,9 @@
<string name="character_sheet__delete_dialog__title">Supprimer la feuille de personnage</string>
<string name="character_sheet__delete_dialog__description">Êtes-vous sûr de vouloir supprimer "%1$s" ?</string>
<string name="character__inventory__add_to_inventory__action">Ajouter un objet</string>
<string name="character__inventory__filter_inventory__label">Filtrer l'inventaire</string>
<string name="character__inventory__add_to_inventory__action">Ajouter un objet</string>
<string name="character__inventory__use__action">Utiliser</string>
<string name="character__inventory__add_to_purse__title">Ajouter à la bourse</string>
<string name="character__inventory__remove_from_purse__title">Retirer de la bourse</string>
<string name="character__inventory__filter_item_inventory__label">Filtrer les objets</string>
@ -193,6 +194,9 @@
<string name="character__inventory__inventory__dialog__action">Ajouter à l'inventaire</string>
<string name="character__inventory__inventory__dialog__count">Quantité</string>
<string name="character__inventory__inventory__dialog__count_action">Modifier</string>
<string name="character__inventory__inventory__dialog__throw_action">Jetter</string>
<string name="character__inventory__inventory__dialog__equip_action">Equiper</string>
<string name="character__inventory__inventory__dialog__consume_action">Utiliser</string>
<string name="character__inventory__description_empty__label">Cet objet n'a pas de description.</string>
<string name="tooltip__characteristics__characteristics">Les caractéristiques constituent les aptitudes innées dun personnage comme son intelligence, sa force, son charisme, etc. Elles ne sont pas acquises, mais peuvent être parfois augmentées par un entraînement ou une utilisation réussie. Les caractéristiques des humains normaux varient de 2 (niveau extrêmement bas) à 20 (maximum du potentiel humain), avec une moyenne de 10 ou 11. Plus une caractéristique est élevée plus le personnage est puissant dans cette aptitude.\nÀ la création de votre personnage, répartissez les valeurs suivantes dans les différentes caractéristiques : 15, 15, 13, 11, 10, 9 et 7.</string>

View file

@ -115,13 +115,35 @@ interface LwaClient {
create: Boolean,
): APIResponse<Unit>
suspend fun deleteInventory(
characterSheetId: String,
): APIResponse<Unit>
suspend fun createInventoryItem(
characterSheetId: String,
itemId: String,
): APIResponse<String>
suspend fun deleteInventory(
suspend fun changeInventoryItemCount(
characterSheetId: String,
inventoryId: String,
count: Float,
): APIResponse<Unit>
suspend fun equipInventoryItem(
characterSheetId: String,
inventoryId: String,
equip: Boolean,
): APIResponse<Unit>
suspend fun consumeInventoryItem(
characterSheetId: String,
inventoryId: String,
): APIResponse<Unit>
suspend fun deleteInventoryItem(
characterSheetId: String,
inventoryId: String,
): APIResponse<Unit>
// Tags

View file

@ -192,18 +192,52 @@ class LwaClientImpl(
}
.body<APIResponse<Unit>>()
@Throws
override suspend fun deleteInventory(characterSheetId: String): APIResponse<Unit> = client
.delete("$root/inventory/delete?characterSheetId=$characterSheetId")
.body()
@Throws
override suspend fun createInventoryItem(
characterSheetId: String,
itemId: String,
): APIResponse<String> = client
.put("$root/inventory/create?characterSheetId=$characterSheetId&itemId=$itemId")
.put("$root/inventory/item/create?characterSheetId=$characterSheetId&itemId=$itemId")
.body<APIResponse<String>>()
@Throws
override suspend fun deleteInventory(characterSheetId: String): APIResponse<Unit> = client
.delete("$root/inventory/delete?characterSheetId=$characterSheetId")
.body()
override suspend fun changeInventoryItemCount(
characterSheetId: String,
inventoryId: String,
count: Float,
): APIResponse<Unit> = client
.put("$root/inventory/item/count?characterSheetId=$characterSheetId&inventoryId=$inventoryId&count=$count")
.body<APIResponse<Unit>>()
@Throws
override suspend fun equipInventoryItem(
characterSheetId: String,
inventoryId: String,
equip: Boolean,
): APIResponse<Unit> = client
.put("$root/inventory/item/equip?characterSheetId=$characterSheetId&inventoryId=$inventoryId&equip=$equip")
.body<APIResponse<Unit>>()
@Throws
override suspend fun consumeInventoryItem(
characterSheetId: String,
inventoryId: String,
): APIResponse<Unit> = client
.put("$root/inventory/item/consume?characterSheetId=$characterSheetId&inventoryId=$inventoryId")
.body<APIResponse<Unit>>()
@Throws
override suspend fun deleteInventoryItem(
characterSheetId: String,
inventoryId: String,
): APIResponse<Unit> = client
.delete("$root/inventory/item/delete?characterSheetId=$characterSheetId&inventoryId=$inventoryId")
.body<APIResponse<Unit>>()
@Throws
override suspend fun getAlterationTags(): APIResponse<List<TagJson>> = client

View file

@ -53,11 +53,52 @@ class InventoryRepository(
}
@Throws
suspend fun deleteItem(
suspend fun changeInventoryItemCount(
characterSheetId: String,
inventoryId: String,
count: Float,
) {
inventoryStore.deleteInventory(
inventoryStore.changeInventoryItemCount(
characterSheetId = characterSheetId,
inventoryId = inventoryId,
count = count,
)
}
@Throws
suspend fun consumeInventoryItem(
characterSheetId: String,
inventoryId: String,
) {
inventoryStore.consumeInventoryItem(
characterSheetId = characterSheetId,
inventoryId = inventoryId,
)
}
@Throws
suspend fun equipInventoryItem(
characterSheetId: String,
inventoryId: String,
) {
val inventory = inventory(characterSheetId = characterSheetId)
val inventoryItem = inventory.items.firstOrNull { it.inventoryId == inventoryId }
inventoryStore.equipInventoryItem(
characterSheetId = characterSheetId,
inventoryId = inventoryId,
equip = inventoryItem?.equipped?.not() ?: true,
)
}
@Throws
suspend fun deleteInventoryItem(
characterSheetId: String,
inventoryId: String,
) {
inventoryStore.deleteInventoryItem(
characterSheetId = characterSheetId,
inventoryId = inventoryId,
)
}
}

View file

@ -73,6 +73,16 @@ class InventoryStore(
}
}
@Throws
suspend fun deleteInventory(
characterSheetId: String,
) {
val request = client.deleteInventory(characterSheetId = characterSheetId)
if (request.success.not()) {
LwaClient.error(error = request)
}
}
@Throws
suspend fun createInventoryItem(
characterSheetId: String,
@ -89,10 +99,60 @@ class InventoryStore(
}
@Throws
suspend fun deleteInventory(
suspend fun changeInventoryItemCount(
characterSheetId: String,
inventoryId: String,
count: Float,
) {
val request = client.deleteInventory(characterSheetId = characterSheetId)
val request = client.changeInventoryItemCount(
characterSheetId = characterSheetId,
inventoryId = inventoryId,
count = count,
)
if (request.success.not()) {
LwaClient.error(error = request)
}
}
@Throws
suspend fun consumeInventoryItem(
characterSheetId: String,
inventoryId: String,
) {
val request = client.consumeInventoryItem(
characterSheetId = characterSheetId,
inventoryId = inventoryId,
)
if (request.success.not()) {
LwaClient.error(error = request)
}
}
@Throws
suspend fun equipInventoryItem(
characterSheetId: String,
inventoryId: String,
equip: Boolean,
) {
val request = client.equipInventoryItem(
characterSheetId = characterSheetId,
inventoryId = inventoryId,
equip = equip,
)
if (request.success.not()) {
LwaClient.error(error = request)
}
}
@Throws
suspend fun deleteInventoryItem(
characterSheetId: String,
inventoryId: String,
) {
val request = client.deleteInventoryItem(
characterSheetId = characterSheetId,
inventoryId = inventoryId,
)
if (request.success.not()) {
LwaClient.error(error = request)
}

View file

@ -1,6 +1,9 @@
package com.pixelized.desktop.lwa.ui.composable.character.item
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@ -42,7 +45,10 @@ import com.pixelized.desktop.lwa.ui.theme.lwa
import com.pixelized.desktop.lwa.utils.rememberSaturationFilter
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.character__inventory__inventory__dialog__action
import lwacharactersheet.composeapp.generated.resources.character__inventory__inventory__dialog__consume_action
import lwacharactersheet.composeapp.generated.resources.character__inventory__inventory__dialog__count_action
import lwacharactersheet.composeapp.generated.resources.character__inventory__inventory__dialog__equip_action
import lwacharactersheet.composeapp.generated.resources.character__inventory__inventory__dialog__throw_action
import lwacharactersheet.composeapp.generated.resources.ic_close_24dp
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource
@ -59,12 +65,14 @@ data class ItemDetailDialogUio(
val count: Float,
// options
val countable: LwaTextFieldUio?,
val consumable: Boolean,
val equipable: Boolean,
)
@Stable
object ItemDetailDialogDefault {
@Stable
val paddings = PaddingValues(all = 16.dp)
val paddings = PaddingValues(top = 16.dp, start = 16.dp, end = 16.dp, bottom = 8.dp)
@Stable
val spacings = 8.dp
@ -78,6 +86,9 @@ fun ItemDetailDialog(
onDismissRequest: () -> Unit,
onConfirm: (ItemDetailDialogUio) -> Unit,
onAddItem: (ItemDetailDialogUio) -> Unit,
onEquip: (ItemDetailDialogUio) -> Unit,
onConsume: (ItemDetailDialogUio) -> Unit,
onThrow: (ItemDetailDialogUio) -> Unit,
) {
LwaDialog(
state = dialog,
@ -85,7 +96,8 @@ fun ItemDetailDialog(
onConfirm = { dialog.value?.let(onConfirm) ?: onDismissRequest },
) { state ->
val layoutDirection = LocalLayoutDirection.current
val end = remember(layoutDirection, paddings) { paddings.calculateEndPadding(layoutDirection) }
val end =
remember(layoutDirection, paddings) { paddings.calculateEndPadding(layoutDirection) }
val top = remember(paddings) { paddings.calculateTopPadding() }
takeIf { state.image?.isNotEmpty() == true }?.let {
@ -140,6 +152,7 @@ fun ItemDetailDialog(
AnimatedContent(
targetState = state.inventoryId,
transitionSpec = { fadeIn() togetherWith fadeOut() },
) {
when (it) {
null -> Row(
@ -158,14 +171,12 @@ fun ItemDetailDialog(
else -> Column(
verticalArrangement = Arrangement.spacedBy(space = spacings)
) {
if (state.countable != null) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(
space = spacings,
Alignment.End,
)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(space = spacings),
verticalAlignment = Alignment.Bottom,
) {
if (state.countable != null) {
LwaTextField(
modifier = Modifier.width(width = 128.dp),
colors = LwaTextFieldColors(
@ -188,6 +199,34 @@ fun ItemDetailDialog(
)
}
}
Spacer(
modifier = Modifier.weight(weight = 1f),
)
TextButton(
onClick = { onThrow(state) },
) {
Text(
text = stringResource(Res.string.character__inventory__inventory__dialog__throw_action),
)
}
if (state.equipable) {
TextButton(
onClick = { onEquip(state) },
) {
Text(
text = stringResource(Res.string.character__inventory__inventory__dialog__equip_action),
)
}
}
if (state.consumable) {
TextButton(
onClick = { onConsume(state) },
) {
Text(
text = stringResource(Res.string.character__inventory__inventory__dialog__consume_action),
)
}
}
}
}
}

View file

@ -37,6 +37,8 @@ class ItemDetailDialogFactory {
countable = takeIf { item.options.stackable }
?.let { createFieldFlow(value = format.format(count)) }
?.createTextField(label = getString(Res.string.character__inventory__inventory__dialog__count)),
consumable = item.options.consumable,
equipable = item.options.equipable,
)
}

View file

@ -83,49 +83,99 @@ class ItemDetailDialogViewModel(
suspend fun onAddInventoryItem(
characterSheetId: String,
itemId: String,
) {
) : Boolean {
try {
// create the inventory item on the server, get the newly create id from that.
val inventoryId = inventoryRepository.createInventoryItem(
characterSheetId = characterSheetId,
itemId = itemId,
)
return true
// update the dialog with the id only if this dialog still correspond to this item. (should always be the case but hey).
if (selectedItemId.value?.let { it.itemId == itemId && it.characterSheetId == characterSheetId } == true) {
selectedItemId.update {
it?.copy(inventoryId = inventoryId)
}
}
// if (selectedItemId.value?.let { it.itemId == itemId && it.characterSheetId == characterSheetId } == true) {
// selectedItemId.update {
// it?.copy(inventoryId = inventoryId)
// }
// }
} catch (exception: Exception) {
val message = ErrorSnackUio.from(exception)
_error.emit(message)
return false
}
}
suspend fun throwInventoryItem(
characterSheetId: String,
inventoryId: String?,
): Boolean {
if (inventoryId == null) return false
try {
inventoryRepository.deleteInventoryItem(
characterSheetId = characterSheetId,
inventoryId = inventoryId,
)
return true
} catch (exception: Exception) {
val message = ErrorSnackUio.from(exception)
_error.emit(message)
return false
}
}
suspend fun consumeInventoryItem(
characterSheetId: String,
inventoryId: String?,
): Boolean {
if (inventoryId == null) return false
try {
inventoryRepository.consumeInventoryItem(
characterSheetId = characterSheetId,
inventoryId = inventoryId,
)
return true
} catch (exception: Exception) {
val message = ErrorSnackUio.from(exception)
_error.emit(message)
return false
}
}
suspend fun equipInventoryItem(
characterSheetId: String,
inventoryId: String?,
) : Boolean {
if (inventoryId == null) return false
try {
inventoryRepository.equipInventoryItem(
characterSheetId = characterSheetId,
inventoryId = inventoryId,
)
return true
} catch (exception: Exception) {
val message = ErrorSnackUio.from(exception)
_error.emit(message)
return false
}
}
suspend fun changeInventoryItemQuantity(
dialog: ItemDetailDialogUio
) : Boolean {
dialog: ItemDetailDialogUio,
): Boolean {
if (dialog.countable?.isError?.value == true) return false
val characterSheetId = dialog.characterSheetId
val inventoryId = dialog.inventoryId ?: return false
val quantity = dialog.countable?.valueFlow?.value ?: return false
val count = factory.parse(quantity = quantity) ?: return false
val inventory = inventoryRepository.inventory(characterSheetId = characterSheetId)
val count = factory.parse(quantity = quantity)
?: quantity.toFloatOrNull()
?: return false
try {
inventoryRepository.updateInventory(
inventory = inventory.copy(
items = inventory.items.toMutableList().also { items ->
val index = items.indexOfFirst { item -> item.inventoryId == inventoryId }
items[index] = items[index].copy(
count = count,
)
}
),
create = false,
inventoryRepository.changeInventoryItemCount(
characterSheetId = characterSheetId,
inventoryId = inventoryId,
count = count
)
return true
} catch (exception: Exception) {

View file

@ -29,7 +29,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import coil3.compose.rememberDrawScopeSizeResolver
import com.pixelized.desktop.lwa.LocalBlurController
import com.pixelized.desktop.lwa.ui.composable.character.inventory.InventoryDialog
import com.pixelized.desktop.lwa.ui.composable.character.inventory.InventoryDialogViewModel
@ -118,6 +117,14 @@ fun CharacterDetailInventory(
inventoryDialogViewModel.showInventoryDialog(
characterSheetId = it,
)
},
onConsume = {
scope.launch {
itemDetailDialogViewModel.consumeInventoryItem(
characterSheetId = it.characterSheetId,
inventoryId = it.inventoryId,
)
}
}
)
}
@ -165,10 +172,14 @@ fun CharacterDetailInventory(
},
onAddItem = { dialog ->
scope.launch {
itemDetailDialogViewModel.onAddInventoryItem(
val result = itemDetailDialogViewModel.onAddInventoryItem(
characterSheetId = dialog.characterSheetId,
itemId = dialog.itemId,
)
if (result) {
blur.hide()
itemDetailDialogViewModel.hideItemDialog()
}
}
},
onConfirm = { dialog ->
@ -181,12 +192,51 @@ fun CharacterDetailInventory(
itemDetailDialogViewModel.hideItemDialog()
}
}
},
onEquip = { dialog ->
scope.launch {
val result = itemDetailDialogViewModel.equipInventoryItem(
characterSheetId = dialog.characterSheetId,
inventoryId = dialog.inventoryId,
)
if (result) {
blur.hide()
itemDetailDialogViewModel.hideItemDialog()
}
}
},
onConsume = { dialog ->
scope.launch {
val result = itemDetailDialogViewModel.consumeInventoryItem(
characterSheetId = dialog.characterSheetId,
inventoryId = dialog.inventoryId,
)
if (result) {
blur.hide()
itemDetailDialogViewModel.hideItemDialog()
}
}
},
onThrow = { dialog ->
scope.launch {
val result = itemDetailDialogViewModel.throwInventoryItem(
characterSheetId = dialog.characterSheetId,
inventoryId = dialog.inventoryId,
)
if (result) {
blur.hide()
itemDetailDialogViewModel.hideItemDialog()
}
}
}
)
ErrorSnackHandler(
error = purseViewModel.error,
)
ErrorSnackHandler(
error = itemDetailDialogViewModel.error,
)
}
@Composable
@ -197,6 +247,7 @@ private fun CharacterDetailInventoryContent(
inventory: CharacterDetailInventoryUio,
onPurse: (String) -> Unit,
onItem: (InventoryItemUio) -> Unit,
onConsume: (InventoryItemUio) -> Unit,
onAddItem: (String) -> Unit,
) {
Box(
@ -257,6 +308,7 @@ private fun CharacterDetailInventoryContent(
modifier = Modifier.animateItem(),
item = it,
onClick = { onItem(it) },
onConsume = { onConsume(it) },
)
}
}

View file

@ -86,6 +86,7 @@ class CharacterDetailInventoryFactory(
label = item.metadata.label,
count = it.count,
equipped = it.equipped,
consumable = item.options.consumable,
tooltips = takeIf { item.metadata.description.isNotEmpty() }?.let {
InventoryItemUio.Tooltips(
label = item.metadata.label,

View file

@ -19,6 +19,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.material.minimumInteractiveComponentSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
@ -38,6 +39,9 @@ import com.pixelized.desktop.lwa.ui.composable.tooltip.TooltipLayout2
import com.pixelized.desktop.lwa.ui.theme.lwa
import com.pixelized.desktop.lwa.utils.extention.ribbon
import com.pixelized.desktop.lwa.utils.rememberSaturationFilter
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.character__inventory__use__action
import org.jetbrains.compose.resources.stringResource
@Stable
data class InventoryItemUio(
@ -47,6 +51,7 @@ data class InventoryItemUio(
val label: String,
val count: Float,
val equipped: Boolean,
val consumable: Boolean,
val tooltips: Tooltips?,
) {
@Stable
@ -60,7 +65,7 @@ data class InventoryItemUio(
@Stable
object GMCharacterPreviewDefault {
@Stable
val paddings = PaddingValues(horizontal = 16.dp, vertical = 4.dp)
val paddings = PaddingValues(horizontal = 16.dp)
@Stable
val spacing: Dp = 4.dp
@ -74,6 +79,7 @@ fun InventoryItem(
spacing: Dp = GMCharacterPreviewDefault.spacing,
item: InventoryItemUio,
onClick: () -> Unit,
onConsume: () -> Unit,
) {
TooltipLayout2(
modifier = modifier,
@ -134,30 +140,41 @@ fun InventoryItem(
.fillMaxWidth()
.padding(paddingValues = padding)
.then(other = modifier),
horizontalArrangement = Arrangement.spacedBy(space = spacing),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.lwa.typography.base.body1,
fontWeight = FontWeight.Bold,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = item.label,
)
AnimatedContent(
modifier = Modifier.alignByBaseline(),
targetState = item.count,
transitionSpec = { fadeIn() togetherWith fadeOut() },
Row(
horizontalArrangement = Arrangement.spacedBy(space = spacing),
) {
when (it) {
0f, 1f -> Unit
else -> Text(
style = MaterialTheme.lwa.typography.base.caption,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = "x${it}",
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.lwa.typography.base.body1,
fontWeight = FontWeight.Bold,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = item.label,
)
AnimatedContent(
modifier = Modifier.alignByBaseline(),
targetState = item.count,
transitionSpec = { fadeIn() togetherWith fadeOut() },
) {
when (it) {
0f, 1f -> Unit
else -> Text(
style = MaterialTheme.lwa.typography.base.caption,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = "x${it}",
)
}
}
}
if (item.consumable) {
TextButton(
onClick = onConsume,
) {
Text(text = stringResource(Res.string.character__inventory__use__action))
}
}
}