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))
}
}
}

View file

@ -1,5 +1,7 @@
package com.pixelized.server.lwa.model.inventory
import com.pixelized.server.lwa.model.item.ItemStore
import com.pixelized.server.lwa.server.exception.BusinessException
import com.pixelized.shared.lwa.model.inventory.Inventory
import com.pixelized.shared.lwa.model.inventory.InventoryJson
import com.pixelized.shared.lwa.model.inventory.factory.InventoryJsonFactory
@ -9,9 +11,11 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import java.util.UUID
class InventoryService(
private val inventoryStore: InventoryStore,
private val itemStore: ItemStore,
private val factory: InventoryJsonFactory,
) {
private val scope = CoroutineScope(Dispatchers.IO + Job())
@ -51,13 +55,185 @@ class InventoryService(
}
@Throws
fun createItem(
fun createInventoryItem(
characterSheetId: String,
itemId: String,
) : String {
return inventoryStore.createItem(
characterSheetId = characterSheetId,
): String {
// get the inventory of the character, if none create one.
val inventory = inventoryStore.inventoryFlow().value[characterSheetId]
?: Inventory.empty(characterSheetId = characterSheetId)
// create an inventoryId.
val inventoryId = inventory.items.createInventoryId()
// create an inventory Item.
val item = Inventory.Item(
inventoryId = inventoryId,
itemId = itemId,
count = 1f,
equipped = false,
)
// update the inventory with the updated item.
val updatedInventory = inventory.copy(
items = inventory.items.toMutableList().also {
it.add(item)
}
)
// save the inventory
inventoryStore.save(
inventory = updatedInventory,
create = false,
)
return inventoryId
}
@Throws
fun changeInventoryItemCount(
characterSheetId: String,
inventoryId: String,
count: Float,
) {
if (count < 0) {
throw BusinessException(
message = "InventoryItem (id:$inventoryId) quantity cannot go below 0.",
)
}
// get the inventory of the character, if none create one.
val inventory = inventoryStore.inventoryFlow().value[characterSheetId]
?: Inventory.empty(characterSheetId = characterSheetId)
// Guard case.
val inventoryItemIndex = inventory.items.indexOfFirst { it.inventoryId == inventoryId }
if (inventoryItemIndex < 0) {
throw BusinessException(
message = "InventoryItem (id:$inventoryId) not found in Inventory(characterSheetId:$characterSheetId).",
)
}
// update the inventory with the updated item.
val updatedInventory = inventory.copy(
items = inventory.items.toMutableList().also { items ->
if (count == 0f) {
items.removeAt(inventoryItemIndex)
} else {
items[inventoryItemIndex] = items[inventoryItemIndex].copy(count = count)
}
}
)
// save the inventory
inventoryStore.save(
inventory = updatedInventory,
create = false,
)
}
@Throws
fun consumeInventoryItem(
characterSheetId: String,
inventoryId: String,
) {
// get the inventory of the character, if none create one.
val inventory = inventoryStore.inventoryFlow().value[characterSheetId]
?: Inventory.empty(characterSheetId = characterSheetId)
// Guard case.
val inventoryItemIndex = inventory.items.indexOfFirst { it.inventoryId == inventoryId }
if (inventoryItemIndex < 0) {
throw BusinessException(
message = "InventoryItem (id:$inventoryId) not found in Inventory(characterSheetId:$characterSheetId).",
)
}
// other Guard case.
val itemId = inventory.items[inventoryItemIndex].itemId
val item = itemStore.item(itemId = itemId)
?: throw BusinessException(message = "Item (id:$itemId) not found.")
if (item.options.consumable.not()) {
throw BusinessException(message = "Item (id:$itemId) is not consumable.")
}
// update the inventory with the updated item.
val updatedInventory = inventory.copy(
items = inventory.items.toMutableList().also { items ->
val inventoryItem = inventory.items[inventoryItemIndex]
if (inventoryItem.count - 1f <= 0f) {
items.removeAt(inventoryItemIndex)
} else {
items[inventoryItemIndex] = inventoryItem.copy(count = inventoryItem.count - 1f)
}
}
)
// save the inventory
inventoryStore.save(
inventory = updatedInventory,
create = false,
)
}
@Throws
fun equipInventoryItem(
characterSheetId: String,
inventoryId: String,
equip: Boolean,
) {
// get the inventory of the character, if none create one.
val inventory = inventoryStore.inventoryFlow().value[characterSheetId]
?: Inventory.empty(characterSheetId = characterSheetId)
// Guard case.
val inventoryItemIndex = inventory.items.indexOfFirst { it.inventoryId == inventoryId }
if (inventoryItemIndex < 0) {
throw BusinessException(
message = "InventoryItem (id:$inventoryId) not found in Inventory(characterSheetId:$characterSheetId).",
)
}
// other Guard case.
val itemId = inventory.items[inventoryItemIndex].itemId
val item = itemStore.item(itemId = itemId)
?: throw BusinessException(message = "Item (id:$itemId) not found.")
if (item.options.equipable.not()) {
throw BusinessException(message = "Item (id:$itemId) is not equipable.")
}
// update the inventory with the updated item.
val updatedInventory = inventory.copy(
items = inventory.items.toMutableList().also { items ->
items[inventoryItemIndex] = inventory.items[inventoryItemIndex].copy(
equipped = equip
)
}
)
// save the inventory
inventoryStore.save(
inventory = updatedInventory,
create = false,
)
}
@Throws
fun deleteInventoryItem(
characterSheetId: String,
inventoryId: String,
) {
// get the inventory of the character, if none create one.
val inventory = inventoryStore.inventoryFlow().value[characterSheetId]
?: Inventory.empty(characterSheetId = characterSheetId)
// Guard case.
val inventoryItemIndex = inventory.items.indexOfFirst { it.inventoryId == inventoryId }
if (inventoryItemIndex < 0) {
throw BusinessException(
message = "InventoryItem (id:$inventoryId) not found in Inventory(characterSheetId:$characterSheetId)",
)
}
// update the inventory with the updated item.
val updatedInventory = inventory.copy(
items = inventory.items.toMutableList().also { items ->
items.removeAt(inventoryItemIndex)
}
)
// save the inventory
inventoryStore.save(
inventory = updatedInventory,
create = false,
)
}
private fun List<Inventory.Item>.createInventoryId(): String {
var inventoryId: String
do {
inventoryId = UUID.randomUUID().toString()
} while (any { inventoryId == it.inventoryId })
return inventoryId
}
}

View file

@ -153,49 +153,7 @@ class InventoryStore(
}
}
@Throws
fun createItem(
characterSheetId: String,
itemId: String,
): String {
val (updatedInventory, inventoryId) = inventoryFlow.value.toMutableMap().let { characters ->
// get the inventory of the character, if none create one.
val inventory = characters[characterSheetId]
?: Inventory.empty(characterSheetId = characterSheetId)
// create an inventoryId.
val inventoryId = inventory.items.createInventoryId()
// create an inventory Item.
val item = Inventory.Item(
inventoryId = inventoryId,
itemId = itemId,
count = 1f,
equipped = false,
)
// update the inventory with the updated item.
inventory.copy(
items = inventory.items.toMutableList().also {
it.add(item)
}
) to inventoryId
}
// save the inventory
save(
inventory = updatedInventory,
create = false,
)
// return the inventory ID.
return inventoryId
}
private fun inventoryFile(id: String): File {
return File("${pathProvider.inventoryPath()}${id}.json")
}
private fun List<Inventory.Item>.createInventoryId(): String {
var inventoryId: String
do {
inventoryId = UUID.randomUUID().toString()
} while (any { inventoryId == it.inventoryId })
return inventoryId
}
}

View file

@ -37,6 +37,10 @@ class ItemStore(
}
}
fun item(itemId: String?): Item? {
return itemFlow.value.firstOrNull { it.id == itemId }
}
fun itemsFlow(): StateFlow<List<Item>> = itemFlow
private fun updateItemsFlow() {

View file

@ -19,8 +19,12 @@ import com.pixelized.server.lwa.server.rest.character.putCharacterAlteration
import com.pixelized.server.lwa.server.rest.character.putCharacterDamage
import com.pixelized.server.lwa.server.rest.character.putCharacterDiminished
import com.pixelized.server.lwa.server.rest.character.putCharacterFatigue
import com.pixelized.server.lwa.server.rest.inventory.createInventory
import com.pixelized.server.lwa.server.rest.inventory.changeInventoryItemCount
import com.pixelized.server.lwa.server.rest.inventory.consumeInventoryItem
import com.pixelized.server.lwa.server.rest.inventory.createInventoryItem
import com.pixelized.server.lwa.server.rest.inventory.equipInventoryItem
import com.pixelized.server.lwa.server.rest.inventory.deleteInventory
import com.pixelized.server.lwa.server.rest.inventory.deleteInventoryItem
import com.pixelized.server.lwa.server.rest.inventory.getInventory
import com.pixelized.server.lwa.server.rest.inventory.putInventory
import com.pixelized.server.lwa.server.rest.item.deleteItem
@ -218,7 +222,7 @@ class LocalServer {
body = engine.deleteAlteration()
)
}
route(path = "item") {
route(path = "/item") {
get(
path = "/all",
body = engine.getItems(),
@ -236,7 +240,7 @@ class LocalServer {
body = engine.deleteItem(),
)
}
route(path = "tag") {
route(path = "/tag") {
get(
path = "/character",
body = engine.getCharacterTags(),
@ -250,7 +254,7 @@ class LocalServer {
body = engine.getItemTags(),
)
}
route(path = "inventory") {
route(path = "/inventory") {
get(
path = "/detail",
body = engine.getInventory(),
@ -259,14 +263,34 @@ class LocalServer {
path = "/update",
body = engine.putInventory()
)
put(
path = "/create",
body = engine.createInventory()
)
delete(
path = "/delete",
body = engine.deleteInventory()
)
route(
path = "/item"
) {
put(
path = "/create",
body = engine.createInventoryItem()
)
put(
path = "/count",
body = engine.changeInventoryItemCount()
)
put(
path = "/consume",
body = engine.consumeInventoryItem()
)
put(
path = "/equip",
body = engine.equipInventoryItem()
)
delete(
path = "/delete",
body = engine.deleteInventoryItem()
)
}
}
}
}

View file

@ -0,0 +1,40 @@
package com.pixelized.server.lwa.server.rest.inventory
import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.characterSheetId
import com.pixelized.server.lwa.utils.extentions.exception
import com.pixelized.server.lwa.utils.extentions.inventoryId
import com.pixelized.server.lwa.utils.extentions.itemId
import com.pixelized.shared.lwa.protocol.rest.APIResponse
import com.pixelized.shared.lwa.protocol.websocket.ApiSynchronisation
import io.ktor.server.response.respond
import io.ktor.server.routing.RoutingContext
fun Engine.deleteInventoryItem(): suspend RoutingContext.() -> Unit {
return {
try {
// get the query parameter
val characterSheetId = call.queryParameters.characterSheetId
val inventoryId = call.queryParameters.inventoryId
// add the item to the inventory.
inventoryService.deleteInventoryItem(
characterSheetId = characterSheetId,
inventoryId = inventoryId,
)
// API & WebSocket responses.
call.respond(
message = APIResponse.success(),
)
webSocket.emit(
value = ApiSynchronisation.InventoryUpdate(
timestamp = System.currentTimeMillis(),
characterSheetId = characterSheetId,
),
)
} catch (exception: Exception) {
call.exception(
exception = exception,
)
}
}
}

View file

@ -0,0 +1,42 @@
package com.pixelized.server.lwa.server.rest.inventory
import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.characterSheetId
import com.pixelized.server.lwa.utils.extentions.count
import com.pixelized.server.lwa.utils.extentions.exception
import com.pixelized.server.lwa.utils.extentions.inventoryId
import com.pixelized.shared.lwa.protocol.rest.APIResponse
import com.pixelized.shared.lwa.protocol.websocket.ApiSynchronisation
import io.ktor.server.response.respond
import io.ktor.server.routing.RoutingContext
fun Engine.changeInventoryItemCount(): suspend RoutingContext.() -> Unit {
return {
try {
// get the query parameter
val characterSheetId = call.queryParameters.characterSheetId
val inventoryId = call.queryParameters.inventoryId
val count = call.queryParameters.count
// add the item to the inventory.
inventoryService.changeInventoryItemCount(
characterSheetId = characterSheetId,
inventoryId = inventoryId,
count = count,
)
// API & WebSocket responses.
call.respond(
message = APIResponse.success(),
)
webSocket.emit(
value = ApiSynchronisation.InventoryUpdate(
timestamp = System.currentTimeMillis(),
characterSheetId = characterSheetId,
),
)
} catch (exception: Exception) {
call.exception(
exception = exception,
)
}
}
}

View file

@ -0,0 +1,39 @@
package com.pixelized.server.lwa.server.rest.inventory
import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.characterSheetId
import com.pixelized.server.lwa.utils.extentions.exception
import com.pixelized.server.lwa.utils.extentions.inventoryId
import com.pixelized.shared.lwa.protocol.rest.APIResponse
import com.pixelized.shared.lwa.protocol.websocket.ApiSynchronisation
import io.ktor.server.response.respond
import io.ktor.server.routing.RoutingContext
fun Engine.consumeInventoryItem(): suspend RoutingContext.() -> Unit {
return {
try {
// get the query parameter
val characterSheetId = call.queryParameters.characterSheetId
val inventoryId = call.queryParameters.inventoryId
// add the item to the inventory.
inventoryService.consumeInventoryItem(
characterSheetId = characterSheetId,
inventoryId = inventoryId,
)
// API & WebSocket responses.
call.respond(
message = APIResponse.success(),
)
webSocket.emit(
value = ApiSynchronisation.InventoryUpdate(
timestamp = System.currentTimeMillis(),
characterSheetId = characterSheetId,
),
)
} catch (exception: Exception) {
call.exception(
exception = exception,
)
}
}
}

View file

@ -0,0 +1,43 @@
package com.pixelized.server.lwa.server.rest.inventory
import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.characterSheetId
import com.pixelized.server.lwa.utils.extentions.equip
import com.pixelized.server.lwa.utils.extentions.exception
import com.pixelized.server.lwa.utils.extentions.inventoryId
import com.pixelized.shared.lwa.protocol.rest.APIResponse
import com.pixelized.shared.lwa.protocol.websocket.ApiSynchronisation
import io.ktor.server.response.respond
import io.ktor.server.routing.RoutingContext
fun Engine.equipInventoryItem(): suspend RoutingContext.() -> Unit {
return {
try {
// get the query parameter
val characterSheetId = call.queryParameters.characterSheetId
val inventoryId = call.queryParameters.inventoryId
val equip = call.queryParameters.equip
// add the item to the inventory.
inventoryService.equipInventoryItem(
characterSheetId = characterSheetId,
inventoryId = inventoryId,
equip = equip,
)
// API & WebSocket responses.
call.respond(
message = APIResponse.success(),
)
webSocket.emit(
value = ApiSynchronisation.InventoryUpdate(
timestamp = System.currentTimeMillis(),
characterSheetId = characterSheetId,
),
)
} catch (exception: Exception) {
call.exception(
exception = exception,
)
}
}
}

View file

@ -9,14 +9,14 @@ import com.pixelized.shared.lwa.protocol.websocket.ApiSynchronisation
import io.ktor.server.response.respond
import io.ktor.server.routing.RoutingContext
fun Engine.createInventory(): suspend RoutingContext.() -> Unit {
fun Engine.createInventoryItem(): suspend RoutingContext.() -> Unit {
return {
try {
// get the query parameter
val characterSheetId = call.queryParameters.characterSheetId
val itemId = call.queryParameters.itemId
// add the item to the inventory.
val inventoryId = inventoryService.createItem(
val inventoryId = inventoryService.createInventoryItem(
characterSheetId = characterSheetId,
itemId = itemId,
)

View file

@ -10,6 +10,7 @@ inline fun <reified T> Parameters.param(
): T {
return when (T::class) {
String::class -> this[name] as? T
Float::class -> this[name]?.toFloatOrNull() as? T
Boolean::class -> this[name]?.toBooleanStrictOrNull() as? T
else -> null
} ?: throw MissingParameterException(
@ -30,12 +31,24 @@ val Parameters.alterationId: String
code = APIResponse.ErrorCode.AlterationId,
)
val Parameters.inventoryId: String
get() = param(
name = "inventoryId",
code = APIResponse.ErrorCode.InventoryId,
)
val Parameters.itemId: String
get() = param(
name = "itemId",
code = APIResponse.ErrorCode.ItemId,
)
val Parameters.count: Float
get() = param(
name = "count",
code = APIResponse.ErrorCode.Count,
)
val Parameters.create: Boolean
get() = param(
name = "create",
@ -47,3 +60,9 @@ val Parameters.active: Boolean
name = "active",
code = APIResponse.ErrorCode.Active,
)
val Parameters.equip: Boolean
get() = param(
name = "equip",
code = APIResponse.ErrorCode.Equip,
)

View file

@ -15,13 +15,16 @@ data class APIResponse<T>(
AlterationId,
AlterationName,
ItemId,
InventoryId,
ItemName,
CharacterSheetId,
Create,
Active,
Equip,
Damage,
Fatigue,
Diminished,
Count,
}
companion object {