Add purse update chatlog message

This commit is contained in:
Thomas Andres Gomez 2025-05-09 15:18:39 +02:00
parent 06c5802d7a
commit 5b633de981
16 changed files with 327 additions and 16 deletions

View file

@ -256,6 +256,8 @@
<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>
<string name="chat__purse_change__spend">dépense</string>
<string name="chat__purse_change__earn">empoche</string>
<string name="settings__title">Paramètres de l'application</string> <string name="settings__title">Paramètres de l'application</string>
<string name="settings__reset_action">Paramètres par défault</string> <string name="settings__reset_action">Paramètres par défault</string>

View file

@ -7,6 +7,7 @@ import com.pixelized.shared.lwa.model.inventory.InventoryJson
import com.pixelized.shared.lwa.model.item.ItemJson import com.pixelized.shared.lwa.model.item.ItemJson
import com.pixelized.shared.lwa.model.tag.TagJson import com.pixelized.shared.lwa.model.tag.TagJson
import com.pixelized.shared.lwa.protocol.rest.APIResponse import com.pixelized.shared.lwa.protocol.rest.APIResponse
import com.pixelized.shared.lwa.protocol.rest.ApiPurseJson
import com.pixelized.shared.lwa.protocol.rest.CharacterPreviewJson import com.pixelized.shared.lwa.protocol.rest.CharacterPreviewJson
interface LwaClient { interface LwaClient {
@ -116,6 +117,10 @@ interface LwaClient {
create: Boolean, create: Boolean,
): APIResponse<Unit> ): APIResponse<Unit>
suspend fun putInventoryPurse(
purse: ApiPurseJson,
): APIResponse<Unit>
suspend fun deleteInventory( suspend fun deleteInventory(
characterSheetId: String, characterSheetId: String,
): APIResponse<Unit> ): APIResponse<Unit>

View file

@ -8,6 +8,7 @@ import com.pixelized.shared.lwa.model.inventory.InventoryJson
import com.pixelized.shared.lwa.model.item.ItemJson import com.pixelized.shared.lwa.model.item.ItemJson
import com.pixelized.shared.lwa.model.tag.TagJson import com.pixelized.shared.lwa.model.tag.TagJson
import com.pixelized.shared.lwa.protocol.rest.APIResponse import com.pixelized.shared.lwa.protocol.rest.APIResponse
import com.pixelized.shared.lwa.protocol.rest.ApiPurseJson
import com.pixelized.shared.lwa.protocol.rest.CharacterPreviewJson import com.pixelized.shared.lwa.protocol.rest.CharacterPreviewJson
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.call.body import io.ktor.client.call.body
@ -199,6 +200,16 @@ class LwaClientImpl(
} }
.body<APIResponse<Unit>>() .body<APIResponse<Unit>>()
@Throws
override suspend fun putInventoryPurse(
purse: ApiPurseJson,
): APIResponse<Unit> = client
.put("$root/inventory/update/purse") {
contentType(ContentType.Application.Json)
setBody(purse)
}
.body<APIResponse<Unit>>()
@Throws @Throws
override suspend fun deleteInventory(characterSheetId: String): APIResponse<Unit> = client override suspend fun deleteInventory(characterSheetId: String): APIResponse<Unit> = client
.delete("$root/inventory/delete?characterSheetId=$characterSheetId") .delete("$root/inventory/delete?characterSheetId=$characterSheetId")

View file

@ -3,7 +3,6 @@ package com.pixelized.desktop.lwa.repository.inventory
import com.pixelized.shared.lwa.model.inventory.Inventory import com.pixelized.shared.lwa.model.inventory.Inventory
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
class InventoryRepository( class InventoryRepository(
@ -50,6 +49,17 @@ class InventoryRepository(
) )
} }
@Throws
suspend fun updateInventoryPurse(
characterSheetId: String,
purse: Inventory.Purse,
) {
inventoryStore.putInventoryPurse(
characterSheetId = characterSheetId,
purse = purse,
)
}
@Throws @Throws
suspend fun createInventoryItem( suspend fun createInventoryItem(
characterSheetId: String, characterSheetId: String,

View file

@ -73,6 +73,22 @@ class InventoryStore(
} }
} }
@Throws
suspend fun putInventoryPurse(
characterSheetId: String,
purse: Inventory.Purse,
) {
val request = client.putInventoryPurse(
purse = factory.convertToApiPurse(
characterSheetId = characterSheetId,
purse = purse,
),
)
if (request.success.not()) {
LwaClient.error(error = request)
}
}
@Throws @Throws
suspend fun deleteInventory( suspend fun deleteInventory(
characterSheetId: String, characterSheetId: String,
@ -163,6 +179,20 @@ class InventoryStore(
private suspend fun handleMessage(message: SocketMessage) { private suspend fun handleMessage(message: SocketMessage) {
when (message) { when (message) {
is ApiSynchronisation.InventoryApiSynchronisation -> when (message) { is ApiSynchronisation.InventoryApiSynchronisation -> when (message) {
is ApiSynchronisation.PurseUpdate -> _inventories.update {
it.toMutableMap().also { inventories ->
inventories[message.characterSheetId]?.let { inventory ->
inventories[message.characterSheetId] = inventory.copy(
purse = Inventory.Purse(
gold = inventory.purse.gold + message.gold,
silver = inventory.purse.silver + message.silver,
copper = inventory.purse.copper + message.copper,
)
)
}
}
}
is ApiSynchronisation.InventoryUpdate -> updateInventoryFlow( is ApiSynchronisation.InventoryUpdate -> updateInventoryFlow(
characterSheetId = message.characterSheetId, characterSheetId = message.characterSheetId,
) )

View file

@ -55,29 +55,26 @@ class PurseDialogViewModel(
if (dialog.enableConfirm.value.not()) { if (dialog.enableConfirm.value.not()) {
return false return false
} }
// Get the player inventory
val inventory = inventoryRepository.inventory(characterSheetId = dialog.characterSheetId)
// compute the new purse // compute the new purse
val sign = if (dialog.add.value) 1 else -1 val sign = if (dialog.add.value) 1 else -1
val goldValue = dialog.gold.valueFlow.value.toIntOrNull() ?: 0 val goldValue = dialog.gold.valueFlow.value.toIntOrNull() ?: 0
val silverValue = dialog.silver.valueFlow.value.toIntOrNull() ?: 0 val silverValue = dialog.silver.valueFlow.value.toIntOrNull() ?: 0
val copperValue = dialog.copper.valueFlow.value.toIntOrNull() ?: 0 val copperValue = dialog.copper.valueFlow.value.toIntOrNull() ?: 0
val purse = Inventory.Purse(
gold = inventory.purse.gold + goldValue * sign,
silver = inventory.purse.silver + silverValue * sign,
copper = inventory.purse.copper + copperValue * sign,
)
// guard case: check if the purse change, not an error case, but avoid useless API call. // guard case: check if the purse change, not an error case, but avoid useless API call.
if (inventory.purse == purse) { if (goldValue == 0 && silverValue == 0 && copperValue == 0) {
return true return true
} }
// build a purse delta
val purse = Inventory.Purse(
gold = goldValue * sign,
silver = silverValue * sign,
copper = copperValue * sign,
)
// API call. // API call.
return try { return try {
inventoryRepository.updateInventory( inventoryRepository.updateInventoryPurse(
inventory = inventory.copy( characterSheetId = dialog.characterSheetId,
purse = purse purse = purse,
),
create = false,
) )
true true
} catch (exception: Exception) { } catch (exception: Exception) {

View file

@ -39,6 +39,8 @@ import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.Characteristic
import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.CharacteristicTextMessageUio import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.CharacteristicTextMessageUio
import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.DiminishedTextMessage import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.DiminishedTextMessage
import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.DiminishedTextMessageUio import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.DiminishedTextMessageUio
import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.PurseTextMessage
import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.PurseTextMessageUio
import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.RollTextMessage import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.RollTextMessage
import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.RollTextMessageUio import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.RollTextMessageUio
import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.TextMessage import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.TextMessage
@ -123,6 +125,7 @@ fun CampaignChat(
) { ) {
when (it) { when (it) {
is RollTextMessageUio -> RollTextMessage(message = it) is RollTextMessageUio -> RollTextMessage(message = it)
is PurseTextMessageUio -> PurseTextMessage(message = it)
is DiminishedTextMessageUio -> DiminishedTextMessage(message = it) is DiminishedTextMessageUio -> DiminishedTextMessage(message = it)
is CharacteristicTextMessageUio -> CharacteristicTextMessage(message = it) is CharacteristicTextMessageUio -> CharacteristicTextMessage(message = it)
} }

View file

@ -4,6 +4,7 @@ import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetReposit
import com.pixelized.desktop.lwa.repository.settings.model.Settings import com.pixelized.desktop.lwa.repository.settings.model.Settings
import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.CharacteristicTextMessageUio import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.CharacteristicTextMessageUio
import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.DiminishedTextMessageUio import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.DiminishedTextMessageUio
import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.PurseTextMessageUio
import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.RollTextMessageUio import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.RollTextMessageUio
import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.TextMessage import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.TextMessage
import com.pixelized.shared.lwa.model.campaign.Campaign import com.pixelized.shared.lwa.model.campaign.Campaign
@ -19,6 +20,8 @@ import lwacharactersheet.composeapp.generated.resources.chat__characteristic_cha
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
import lwacharactersheet.composeapp.generated.resources.chat__purse_change__earn
import lwacharactersheet.composeapp.generated.resources.chat__purse_change__spend
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import kotlin.math.abs import kotlin.math.abs
@ -143,7 +146,33 @@ class TextMessageFactory(
is GameAdminEvent -> null is GameAdminEvent -> null
is ApiSynchronisation -> null is ApiSynchronisation -> when (message) {
is ApiSynchronisation.PurseUpdate -> {
// only display the message if the character is in the party.
val isInParty = campaign.characters.contains(message.characterSheetId)
if (isInParty.not()) return null
// get the character sheet
val sheet = characterSheetRepository
.characterDetail(characterSheetId = message.characterSheetId)
?: return null
PurseTextMessageUio(
id = "${message.timestamp}-${message.characterSheetId}-Purse",
timestamp = formatTime.format(message.timestamp),
character = sheet.name,
action = when (message.add) {
true -> Res.string.chat__purse_change__earn
else -> Res.string.chat__purse_change__spend
},
gold = message.gold.takeIf { it != 0 }?.let { abs(it) },
silver = message.silver.takeIf { it != 0 }?.let { abs(it) },
copper = message.copper.takeIf { it != 0 }?.let { abs(it) },
)
}
else -> null
}
} }
} }
} }

View file

@ -0,0 +1,124 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.text.messages
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.theme.lwa
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.ic_copper_32px
import lwacharactersheet.composeapp.generated.resources.ic_gold_32px
import lwacharactersheet.composeapp.generated.resources.ic_silver_32px
import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource
@Stable
data class PurseTextMessageUio(
override val id: String,
override val timestamp: String,
val character: String,
val action: StringResource,
val gold: Int?,
val silver: Int?,
val copper: Int?,
) : TextMessage
@Composable
fun PurseTextMessage(
modifier: Modifier = Modifier,
message: PurseTextMessageUio,
) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(space = 3.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.lwa.typography.chat.timestamp,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = message.timestamp,
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.lwa.typography.chat.timestamp,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = ">",
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.lwa.typography.chat.text,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = message.character,
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.lwa.typography.chat.text,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = stringResource(message.action),
)
message.gold?.let {
Row {
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.lwa.typography.chat.text,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = "$it",
)
Image(
painter = painterResource(Res.drawable.ic_gold_32px),
modifier = Modifier.padding(bottom = 2.dp).size(size = 16.dp),
contentDescription = null,
)
}
}
message.silver?.let {
Row {
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.lwa.typography.chat.text,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = "$it",
)
Image(
painter = painterResource(Res.drawable.ic_silver_32px),
modifier = Modifier.padding(bottom = 2.dp).size(size = 16.dp),
contentDescription = null,
)
}
}
message.copper?.let {
Row {
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.lwa.typography.chat.text,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = "$it",
)
Image(
painter = painterResource(Res.drawable.ic_copper_32px),
modifier = Modifier.padding(bottom = 2.dp).size(size = 16.dp),
contentDescription = null,
)
}
}
}
}

View file

@ -88,7 +88,7 @@ fun GMActionContent(
GMAction( GMAction(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
icon = Res.drawable.ic_sync_24dp, icon = Res.drawable.ic_sync_24dp,
label = "Syncrhonization du serveur", label = "Synchronization du serveur",
onAction = onServerSync, onAction = onServerSync,
) )
GMAction( GMAction(

View file

@ -5,6 +5,7 @@ import com.pixelized.server.lwa.server.exception.BusinessException
import com.pixelized.shared.lwa.model.inventory.Inventory import com.pixelized.shared.lwa.model.inventory.Inventory
import com.pixelized.shared.lwa.model.inventory.InventoryJson import com.pixelized.shared.lwa.model.inventory.InventoryJson
import com.pixelized.shared.lwa.model.inventory.factory.InventoryJsonFactory import com.pixelized.shared.lwa.model.inventory.factory.InventoryJsonFactory
import com.pixelized.shared.lwa.protocol.rest.ApiPurseJson
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -38,6 +39,25 @@ class InventoryService(
?: factory.convertToJson(Inventory.empty(characterSheetId = characterSheetId)) ?: factory.convertToJson(Inventory.empty(characterSheetId = characterSheetId))
} }
@Throws
fun updatePurse(
purse: ApiPurseJson,
) {
val inventory = inventory(
characterSheetId = purse.characterSheetId
)
inventoryStore.save(
inventory = inventory.copy(
purse = inventory.purse.copy(
gold = inventory.purse.gold + purse.gold,
silver = inventory.purse.silver + purse.silver,
copper = inventory.purse.copper + purse.copper,
),
),
create = false,
)
}
@Throws @Throws
fun save( fun save(
inventoryJson: InventoryJson, inventoryJson: InventoryJson,

View file

@ -28,6 +28,7 @@ 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.deleteInventoryItem
import com.pixelized.server.lwa.server.rest.inventory.getInventory 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.inventory.putInventory
import com.pixelized.server.lwa.server.rest.inventory.putPurse
import com.pixelized.server.lwa.server.rest.item.deleteItem import com.pixelized.server.lwa.server.rest.item.deleteItem
import com.pixelized.server.lwa.server.rest.item.getItem import com.pixelized.server.lwa.server.rest.item.getItem
import com.pixelized.server.lwa.server.rest.item.getItems import com.pixelized.server.lwa.server.rest.item.getItems
@ -272,6 +273,10 @@ class LocalServer {
path = "/update", path = "/update",
body = engine.putInventory() body = engine.putInventory()
) )
put(
path = "/update/purse",
body = engine.putPurse(),
)
delete( delete(
path = "/delete", path = "/delete",
body = engine.deleteInventory() body = engine.deleteInventory()

View file

@ -0,0 +1,41 @@
package com.pixelized.server.lwa.server.rest.inventory
import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.exception
import com.pixelized.shared.lwa.protocol.rest.APIResponse
import com.pixelized.shared.lwa.protocol.rest.ApiPurseJson
import com.pixelized.shared.lwa.protocol.websocket.ApiSynchronisation
import io.ktor.server.request.receive
import io.ktor.server.response.respond
import io.ktor.server.routing.RoutingContext
fun Engine.putPurse(): suspend RoutingContext.() -> Unit {
return {
try {
// get the query parameter
val form = call.receive<ApiPurseJson>()
// get the character inventory
inventoryService.updatePurse(
purse = form,
)
// send it back to the user.
call.respond(
message = APIResponse.success()
)
webSocket.emit(
value = ApiSynchronisation.PurseUpdate(
timestamp = System.currentTimeMillis(),
characterSheetId = form.characterSheetId,
add = (form.gold * 10000 + form.silver * 100 + form.copper) >= 0,
gold = form.gold,
silver = form.silver,
copper = form.copper,
),
)
} catch (exception: Exception) {
call.exception(
exception = exception
)
}
}
}

View file

@ -3,6 +3,7 @@ package com.pixelized.shared.lwa.model.inventory.factory
import com.pixelized.shared.lwa.model.inventory.Inventory import com.pixelized.shared.lwa.model.inventory.Inventory
import com.pixelized.shared.lwa.model.inventory.InventoryJson import com.pixelized.shared.lwa.model.inventory.InventoryJson
import com.pixelized.shared.lwa.model.inventory.InventoryJsonV1 import com.pixelized.shared.lwa.model.inventory.InventoryJsonV1
import com.pixelized.shared.lwa.protocol.rest.ApiPurseJson
class InventoryJsonFactory( class InventoryJsonFactory(
private val v1: InventoryJsonFactoryV1, private val v1: InventoryJsonFactoryV1,
@ -16,4 +17,16 @@ class InventoryJsonFactory(
fun convertToJson(inventory: Inventory): InventoryJson { fun convertToJson(inventory: Inventory): InventoryJson {
return v1.convertToJson(inventory = inventory) return v1.convertToJson(inventory = inventory)
} }
fun convertToApiPurse(
characterSheetId: String,
purse: Inventory.Purse,
): ApiPurseJson {
return ApiPurseJson(
characterSheetId = characterSheetId,
gold = purse.gold,
silver = purse.silver,
copper = purse.copper,
)
}
} }

View file

@ -0,0 +1,11 @@
package com.pixelized.shared.lwa.protocol.rest
import kotlinx.serialization.Serializable
@Serializable
data class ApiPurseJson(
val characterSheetId: String,
val gold: Int,
val silver: Int,
val copper: Int,
)

View file

@ -53,6 +53,16 @@ sealed interface ApiSynchronisation : SocketMessage {
@Serializable @Serializable
sealed interface InventoryApiSynchronisation : ApiSynchronisation, CharacterSheetIdMessage sealed interface InventoryApiSynchronisation : ApiSynchronisation, CharacterSheetIdMessage
@Serializable
data class PurseUpdate(
override val timestamp: Long,
override val characterSheetId: String,
val add : Boolean,
val gold: Int,
val silver: Int,
val copper: Int,
) : InventoryApiSynchronisation
@Serializable @Serializable
data class InventoryUpdate( data class InventoryUpdate(
override val timestamp: Long, override val timestamp: Long,