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__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__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__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.tag.TagJson
import com.pixelized.shared.lwa.protocol.rest.APIResponse
import com.pixelized.shared.lwa.protocol.rest.ApiPurseJson
import com.pixelized.shared.lwa.protocol.rest.CharacterPreviewJson
interface LwaClient {
@ -116,6 +117,10 @@ interface LwaClient {
create: Boolean,
): APIResponse<Unit>
suspend fun putInventoryPurse(
purse: ApiPurseJson,
): APIResponse<Unit>
suspend fun deleteInventory(
characterSheetId: String,
): 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.tag.TagJson
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 io.ktor.client.HttpClient
import io.ktor.client.call.body
@ -199,6 +200,16 @@ class LwaClientImpl(
}
.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
override suspend fun deleteInventory(characterSheetId: String): APIResponse<Unit> = client
.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 kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
class InventoryRepository(
@ -50,6 +49,17 @@ class InventoryRepository(
)
}
@Throws
suspend fun updateInventoryPurse(
characterSheetId: String,
purse: Inventory.Purse,
) {
inventoryStore.putInventoryPurse(
characterSheetId = characterSheetId,
purse = purse,
)
}
@Throws
suspend fun createInventoryItem(
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
suspend fun deleteInventory(
characterSheetId: String,
@ -163,6 +179,20 @@ class InventoryStore(
private suspend fun handleMessage(message: SocketMessage) {
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(
characterSheetId = message.characterSheetId,
)

View file

@ -55,29 +55,26 @@ class PurseDialogViewModel(
if (dialog.enableConfirm.value.not()) {
return false
}
// Get the player inventory
val inventory = inventoryRepository.inventory(characterSheetId = dialog.characterSheetId)
// compute the new purse
val sign = if (dialog.add.value) 1 else -1
val goldValue = dialog.gold.valueFlow.value.toIntOrNull() ?: 0
val silverValue = dialog.silver.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.
if (inventory.purse == purse) {
if (goldValue == 0 && silverValue == 0 && copperValue == 0) {
return true
}
// build a purse delta
val purse = Inventory.Purse(
gold = goldValue * sign,
silver = silverValue * sign,
copper = copperValue * sign,
)
// API call.
return try {
inventoryRepository.updateInventory(
inventory = inventory.copy(
purse = purse
),
create = false,
inventoryRepository.updateInventoryPurse(
characterSheetId = dialog.characterSheetId,
purse = purse,
)
true
} 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.DiminishedTextMessage
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.RollTextMessageUio
import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.TextMessage
@ -123,6 +125,7 @@ fun CampaignChat(
) {
when (it) {
is RollTextMessageUio -> RollTextMessage(message = it)
is PurseTextMessageUio -> PurseTextMessage(message = it)
is DiminishedTextMessageUio -> DiminishedTextMessage(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.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.PurseTextMessageUio
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.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__pp_down
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 kotlin.math.abs
@ -143,7 +146,33 @@ class TextMessageFactory(
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(
modifier = Modifier.fillMaxWidth(),
icon = Res.drawable.ic_sync_24dp,
label = "Syncrhonization du serveur",
label = "Synchronization du serveur",
onAction = onServerSync,
)
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.InventoryJson
import com.pixelized.shared.lwa.model.inventory.factory.InventoryJsonFactory
import com.pixelized.shared.lwa.protocol.rest.ApiPurseJson
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@ -38,6 +39,25 @@ class InventoryService(
?: 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
fun save(
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.getInventory
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.getItem
import com.pixelized.server.lwa.server.rest.item.getItems
@ -272,6 +273,10 @@ class LocalServer {
path = "/update",
body = engine.putInventory()
)
put(
path = "/update/purse",
body = engine.putPurse(),
)
delete(
path = "/delete",
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.InventoryJson
import com.pixelized.shared.lwa.model.inventory.InventoryJsonV1
import com.pixelized.shared.lwa.protocol.rest.ApiPurseJson
class InventoryJsonFactory(
private val v1: InventoryJsonFactoryV1,
@ -16,4 +17,16 @@ class InventoryJsonFactory(
fun convertToJson(inventory: Inventory): InventoryJson {
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
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
data class InventoryUpdate(
override val timestamp: Long,