Add visibility toggle for player & npc + refactor WebSocketMessage.

This commit is contained in:
Andres Gomez, Thomas (ITDV RL) 2025-03-19 15:19:31 +01:00
parent 2d164b265e
commit 4c37d8b937
47 changed files with 475 additions and 304 deletions

View file

@ -6,8 +6,8 @@ import com.pixelized.shared.lwa.model.alteration.Alteration
import com.pixelized.shared.lwa.model.alteration.AlterationJsonFactory import com.pixelized.shared.lwa.model.alteration.AlterationJsonFactory
import com.pixelized.shared.lwa.model.campaign.Campaign.CharacterInstance import com.pixelized.shared.lwa.model.campaign.Campaign.CharacterInstance
import com.pixelized.shared.lwa.model.campaign.factory.CampaignJsonFactory import com.pixelized.shared.lwa.model.campaign.factory.CampaignJsonFactory
import com.pixelized.shared.lwa.protocol.websocket.Message import com.pixelized.shared.lwa.protocol.websocket.SocketMessage
import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation import com.pixelized.shared.lwa.protocol.websocket.ToggleActiveAlteration
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -88,15 +88,17 @@ class AlterationStore(
) )
} }
private suspend fun handleMessage(message: Message) { private suspend fun handleMessage(message: SocketMessage) {
when (val payload = message.value) { when (message) {
is RestSynchronisation.ToggleActiveAlteration -> { is ToggleActiveAlteration -> {
setActiveAlteration( setActiveAlteration(
characterInstanceId = campaignJsonFactory.characterInstanceIdFromJson( characterInstanceId = CharacterInstance.Id(
characterInstanceIdJson = payload.characterId, prefix = message.prefix,
characterSheetId = message.characterSheetId,
instanceId = message.instanceId,
), ),
alterationId = payload.alterationId, alterationId = message.alterationId,
active = payload.active, active = message.active,
) )
} }

View file

@ -6,9 +6,10 @@ import com.pixelized.shared.lwa.model.campaign.Campaign
import com.pixelized.shared.lwa.model.campaign.character import com.pixelized.shared.lwa.model.campaign.character
import com.pixelized.shared.lwa.model.campaign.factory.CampaignJsonFactory import com.pixelized.shared.lwa.model.campaign.factory.CampaignJsonFactory
import com.pixelized.shared.lwa.model.campaign.npc import com.pixelized.shared.lwa.model.campaign.npc
import com.pixelized.shared.lwa.protocol.websocket.Message import com.pixelized.shared.lwa.protocol.websocket.SocketMessage
import com.pixelized.shared.lwa.protocol.websocket.payload.CampaignMessage import com.pixelized.shared.lwa.protocol.websocket.CampaignMessage
import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation import com.pixelized.shared.lwa.protocol.websocket.GameMasterEvent
import com.pixelized.shared.lwa.protocol.websocket.RestSynchronisation
import com.pixelized.shared.lwa.usecase.CampaignUseCase import com.pixelized.shared.lwa.usecase.CampaignUseCase
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -106,28 +107,46 @@ class CampaignStore(
// region : WebSocket message Handling. // region : WebSocket message Handling.
private suspend fun handleMessage(message: Message) { private suspend fun handleMessage(message: SocketMessage) {
when (val payload = message.value) { when (message) {
is RestSynchronisation.Campaign -> { is RestSynchronisation.Campaign -> {
campaign(update = true) campaign(update = true)
} }
is CampaignMessage -> { is CampaignMessage -> {
val instanceId = Campaign.CharacterInstance.Id( val instanceId = Campaign.CharacterInstance.Id(
prefix = payload.prefix, prefix = message.prefix,
characterSheetId = payload.characterSheetId, characterSheetId = message.characterSheetId,
instanceId = payload.instanceId, instanceId = message.instanceId,
) )
when (payload) { when (message) {
is CampaignMessage.UpdateCharacteristic -> updateCharacteristic( is CampaignMessage.UpdateCharacteristic -> updateCharacteristic(
characterInstanceId = instanceId, characterInstanceId = instanceId,
characteristic = factory.convertFromJson(json = payload.characteristic), characteristic = factory.convertFromJson(json = message.characteristic),
value = payload.value, value = message.value,
) )
is CampaignMessage.UpdateDiminished -> updateDiminished( is CampaignMessage.UpdateDiminished -> updateDiminished(
characterInstanceId = instanceId, characterInstanceId = instanceId,
diminished = payload.diminished, diminished = message.diminished,
)
}
}
is GameMasterEvent -> when (message) {
is GameMasterEvent.ToggleNpc -> {
_campaignFlow.value = _campaignFlow.value.copy(
options = _campaignFlow.value.options.copy(
showNpcs = _campaignFlow.value.options.showNpcs.not()
)
)
}
is GameMasterEvent.TogglePlayer -> {
_campaignFlow.value = _campaignFlow.value.copy(
options = _campaignFlow.value.options.copy(
showParty = _campaignFlow.value.options.showParty.not()
)
) )
} }
} }

View file

@ -5,9 +5,9 @@ import com.pixelized.desktop.lwa.repository.network.NetworkRepository
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJsonFactory import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJsonFactory
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetPreview import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetPreview
import com.pixelized.shared.lwa.protocol.websocket.Message import com.pixelized.shared.lwa.protocol.websocket.SocketMessage
import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation import com.pixelized.shared.lwa.protocol.websocket.RestSynchronisation
import com.pixelized.shared.lwa.protocol.websocket.payload.UpdateSkillUsageMessage import com.pixelized.shared.lwa.protocol.websocket.UpdateSkillUsageMessage
import com.pixelized.shared.lwa.usecase.CharacterSheetUseCase import com.pixelized.shared.lwa.usecase.CharacterSheetUseCase
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -97,29 +97,29 @@ class CharacterSheetStore(
// endregion // endregion
// region: WebSocket & data update. // region: WebSocket & data update.
private suspend fun handleMessage(message: Message) { private suspend fun handleMessage(message: SocketMessage) {
when (val payload = message.value) { when (message) {
is RestSynchronisation.CharacterUpdate -> { is RestSynchronisation.CharacterSheetUpdate -> {
getCharacterSheet( getCharacterSheet(
characterSheetId = payload.id, characterSheetId = message.characterSheetId,
forceUpdate = true, forceUpdate = true,
) )
if (_previewFlow.value.firstOrNull { it.characterSheetId == payload.id } == null) { if (_previewFlow.value.firstOrNull { it.characterSheetId == message.characterSheetId } == null) {
charactersPreview() charactersPreview()
} }
} }
is RestSynchronisation.CharacterDelete -> { is RestSynchronisation.CharacterSheetDelete -> {
_previewFlow.value = previewFlow.value.toMutableList() _previewFlow.value = previewFlow.value.toMutableList()
.also { sheets -> sheets.removeIf { it.characterSheetId == payload.characterId } } .also { sheets -> sheets.removeIf { it.characterSheetId == message.characterSheetId } }
_detailFlow.delete(payload.characterId) _detailFlow.delete(message.characterSheetId)
} }
is UpdateSkillUsageMessage -> { is UpdateSkillUsageMessage -> {
updateCharacterSkillChange( updateCharacterSkillChange(
characterId = payload.characterSheetId, characterId = message.characterSheetId,
skillId = payload.skillId, skillId = message.skillId,
used = payload.used, used = message.used,
) )
} }

View file

@ -2,8 +2,7 @@ package com.pixelized.desktop.lwa.repository.network
import com.pixelized.desktop.lwa.repository.network.helper.connectWebSocket import com.pixelized.desktop.lwa.repository.network.helper.connectWebSocket
import com.pixelized.desktop.lwa.repository.settings.SettingsRepository import com.pixelized.desktop.lwa.repository.settings.SettingsRepository
import com.pixelized.shared.lwa.protocol.websocket.Message import com.pixelized.shared.lwa.protocol.websocket.SocketMessage
import com.pixelized.shared.lwa.protocol.websocket.payload.MessagePayload
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.engine.okhttp.OkHttp import io.ktor.client.engine.okhttp.OkHttp
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
@ -20,7 +19,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
class NetworkRepository( class NetworkRepository(
@ -31,9 +29,9 @@ class NetworkRepository(
private var networkJob: Job? = null private var networkJob: Job? = null
private var client: HttpClient? = null private var client: HttpClient? = null
private val outgoingMessageBuffer = MutableSharedFlow<Message>() private val outgoingMessageBuffer = MutableSharedFlow<SocketMessage>()
private val incomingMessageBuffer = MutableSharedFlow<Message>() private val incomingMessageBuffer = MutableSharedFlow<SocketMessage>()
val data: SharedFlow<Message> get() = incomingMessageBuffer val data: SharedFlow<SocketMessage> get() = incomingMessageBuffer
private val _status = MutableStateFlow(Status.DISCONNECTED) private val _status = MutableStateFlow(Status.DISCONNECTED)
val status: StateFlow<Status> get() = _status val status: StateFlow<Status> get() = _status
@ -67,7 +65,7 @@ class NetworkRepository(
incoming.consumeEach { frame -> incoming.consumeEach { frame ->
if (frame is Frame.Text) { if (frame is Frame.Text) {
val data = frame.readText() val data = frame.readText()
val message = json.decodeFromString<Message>(data) val message = json.decodeFromString<SocketMessage>(data)
incomingMessageBuffer.emit(message) incomingMessageBuffer.emit(message)
} }
} }
@ -96,14 +94,9 @@ class NetworkRepository(
} }
suspend fun share( suspend fun share(
playerName: String = settingsRepository.settings().playerName, message: SocketMessage,
payload: MessagePayload,
) { ) {
if (status.value == Status.CONNECTED) { if (status.value == Status.CONNECTED) {
val message = Message(
from = playerName,
value = payload,
)
// emit the message into the outgoing buffer // emit the message into the outgoing buffer
outgoingMessageBuffer.emit(message) outgoingMessageBuffer.emit(message)
} }

View file

@ -1,21 +1,23 @@
package com.pixelized.desktop.lwa.repository.roll_history package com.pixelized.desktop.lwa.repository.roll_history
import com.pixelized.desktop.lwa.repository.network.NetworkRepository import com.pixelized.desktop.lwa.repository.network.NetworkRepository
import com.pixelized.shared.lwa.protocol.websocket.payload.RollMessage import com.pixelized.shared.lwa.protocol.websocket.RollMessage
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.shareIn
class RollHistoryRepository( class RollHistoryRepository(
private val network: NetworkRepository, network: NetworkRepository,
) { ) {
private val scope = CoroutineScope(Dispatchers.IO) private val scope = CoroutineScope(Dispatchers.IO)
val rolls: SharedFlow<RollMessage> = network.data val rolls: SharedFlow<RollMessage> = network.data
.mapNotNull { it.value as? RollMessage } .filterIsInstance(RollMessage::class)
.shareIn( .shareIn(
scope = scope, scope = scope,
started = SharingStarted.Eagerly, started = SharingStarted.Eagerly,

View file

@ -13,7 +13,7 @@ import com.pixelized.shared.lwa.model.campaign.Campaign
import com.pixelized.shared.lwa.model.campaign.Campaign.CharacterInstance.Characteristic import com.pixelized.shared.lwa.model.campaign.Campaign.CharacterInstance.Characteristic
import com.pixelized.shared.lwa.model.campaign.factory.CampaignJsonFactory import com.pixelized.shared.lwa.model.campaign.factory.CampaignJsonFactory
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet
import com.pixelized.shared.lwa.protocol.websocket.payload.CampaignMessage import com.pixelized.shared.lwa.protocol.websocket.CampaignMessage
class CharacterDetailCharacteristicDialogViewModel( class CharacterDetailCharacteristicDialogViewModel(
private val characterSheetRepository: CharacterSheetRepository, private val characterSheetRepository: CharacterSheetRepository,
@ -79,7 +79,8 @@ class CharacterDetailCharacteristicDialogViewModel(
) )
// share the data through the websocket. // share the data through the websocket.
network.share( network.share(
payload = CampaignMessage.UpdateCharacteristic( message = CampaignMessage.UpdateCharacteristic(
timestamp = System.currentTimeMillis(),
prefix = characterInstanceId.prefix, prefix = characterInstanceId.prefix,
characterSheetId = characterInstanceId.characterSheetId, characterSheetId = characterInstanceId.characterSheetId,
instanceId = characterInstanceId.instanceId, instanceId = characterInstanceId.instanceId,

View file

@ -13,7 +13,7 @@ import com.pixelized.desktop.lwa.ui.overlay.roll.DifficultyUio.Difficulty
import com.pixelized.desktop.lwa.ui.overlay.roll.RollAction.RollActionUio import com.pixelized.desktop.lwa.ui.overlay.roll.RollAction.RollActionUio
import com.pixelized.shared.lwa.model.AlteredCharacterSheet import com.pixelized.shared.lwa.model.AlteredCharacterSheet
import com.pixelized.shared.lwa.model.AlteredCharacterSheetFactory import com.pixelized.shared.lwa.model.AlteredCharacterSheetFactory
import com.pixelized.shared.lwa.protocol.websocket.payload.RollMessage import com.pixelized.shared.lwa.protocol.websocket.RollMessage
import com.pixelized.shared.lwa.usecase.ExpressionUseCase import com.pixelized.shared.lwa.usecase.ExpressionUseCase
import com.pixelized.shared.lwa.usecase.SkillStepUseCase import com.pixelized.shared.lwa.usecase.SkillStepUseCase
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -31,6 +31,7 @@ import lwacharactersheet.composeapp.generated.resources.roll_page__failure
import lwacharactersheet.composeapp.generated.resources.roll_page__special_success import lwacharactersheet.composeapp.generated.resources.roll_page__special_success
import lwacharactersheet.composeapp.generated.resources.roll_page__success import lwacharactersheet.composeapp.generated.resources.roll_page__success
import org.jetbrains.compose.resources.getString import org.jetbrains.compose.resources.getString
import java.util.UUID
class RollViewModel( class RollViewModel(
private val characterSheetRepository: CharacterSheetRepository, private val characterSheetRepository: CharacterSheetRepository,
@ -267,7 +268,8 @@ class RollViewModel(
val rollAction = rollAction ?: return val rollAction = rollAction ?: return
val payload = RollMessage( val payload = RollMessage(
id = RollMessage.RollId.create(), timestamp = System.currentTimeMillis(),
uuid = UUID.randomUUID().toString(),
prefix = rollAction.characterInstanceId.prefix, prefix = rollAction.characterInstanceId.prefix,
characterSheetId = rollAction.characterInstanceId.characterSheetId, characterSheetId = rollAction.characterInstanceId.characterSheetId,
instanceId = rollAction.characterInstanceId.instanceId, instanceId = rollAction.characterInstanceId.instanceId,
@ -294,7 +296,7 @@ class RollViewModel(
}, },
) )
networkRepository.share( networkRepository.share(
payload = payload, message = payload,
) )
} }
} }

View file

@ -7,7 +7,7 @@ import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.desktop.lwa.repository.network.NetworkRepository import com.pixelized.desktop.lwa.repository.network.NetworkRepository
import com.pixelized.shared.lwa.model.campaign.Campaign import com.pixelized.shared.lwa.model.campaign.Campaign
import com.pixelized.shared.lwa.protocol.websocket.payload.UpdateSkillUsageMessage import com.pixelized.shared.lwa.protocol.websocket.UpdateSkillUsageMessage
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@ -84,7 +84,8 @@ class CharacterDetailViewModel(
) { ) {
val characterSheetId = displayedCharacterId.value?.characterSheetId ?: return val characterSheetId = displayedCharacterId.value?.characterSheetId ?: return
network.share( network.share(
payload = UpdateSkillUsageMessage( message = UpdateSkillUsageMessage(
timestamp = System.currentTimeMillis(),
characterSheetId = characterSheetId, characterSheetId = characterSheetId,
skillId = skillId, skillId = skillId,
used = used.not(), used = used.not(),

View file

@ -9,7 +9,7 @@ import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository
import com.pixelized.desktop.lwa.repository.network.NetworkRepository import com.pixelized.desktop.lwa.repository.network.NetworkRepository
import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.dialog.DiminishedStatDialogUio import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.dialog.DiminishedStatDialogUio
import com.pixelized.shared.lwa.model.campaign.Campaign import com.pixelized.shared.lwa.model.campaign.Campaign
import com.pixelized.shared.lwa.protocol.websocket.payload.CampaignMessage import com.pixelized.shared.lwa.protocol.websocket.CampaignMessage
import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.character_sheet__diminished__label import lwacharactersheet.composeapp.generated.resources.character_sheet__diminished__label
import org.jetbrains.compose.resources.getString import org.jetbrains.compose.resources.getString
@ -56,7 +56,8 @@ class CharacterDiminishedViewModel(
) { ) {
val diminished = dialog.value().text.toIntOrNull() ?: 0 val diminished = dialog.value().text.toIntOrNull() ?: 0
networkRepository.share( networkRepository.share(
payload = CampaignMessage.UpdateDiminished( message = CampaignMessage.UpdateDiminished(
timestamp = System.currentTimeMillis(),
prefix = dialog.characterInstanceId.prefix, prefix = dialog.characterInstanceId.prefix,
characterSheetId = dialog.characterInstanceId.characterSheetId, characterSheetId = dialog.characterInstanceId.characterSheetId,
instanceId = dialog.characterInstanceId.instanceId, instanceId = dialog.characterInstanceId.instanceId,

View file

@ -22,7 +22,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapMerge import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
@ -53,7 +53,7 @@ abstract class CharacterRibbonViewModel(
*/ */
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
val characters: StateFlow<List<CharacterPortraitUio>> = campaignRepository.campaignFlow val characters: StateFlow<List<CharacterPortraitUio>> = campaignRepository.campaignFlow
.flatMapMerge { campaign -> .flatMapLatest { campaign ->
when (campaign.data.isEmpty()) { when (campaign.data.isEmpty()) {
true -> flowOf(emptyList()) true -> flowOf(emptyList())
else -> combine<CharacterPortraitUio?, List<CharacterPortraitUio>>( else -> combine<CharacterPortraitUio?, List<CharacterPortraitUio>>(

View file

@ -24,7 +24,7 @@ class NpcRibbonViewModel(
campaignRepository = campaignRepository, campaignRepository = campaignRepository,
ribbonFactory = ribbonFactory, ribbonFactory = ribbonFactory,
) { ) {
override val Campaign.data get() = npcs override val Campaign.data get() = if (options.showNpcs) npcs else emptyMap()
override val enableCharacterSheet = false override val enableCharacterSheet = false
override val enableCharacterStats = false override val enableCharacterStats = false

View file

@ -24,7 +24,7 @@ class PlayerRibbonViewModel(
campaignRepository = campaignRepository, campaignRepository = campaignRepository,
ribbonFactory = ribbonFactory, ribbonFactory = ribbonFactory,
) { ) {
override val Campaign.data get() = characters override val Campaign.data get() = if (options.showParty) characters else emptyMap()
override val enableCharacterSheet = true override val enableCharacterSheet = true
override val enableCharacterStats = true override val enableCharacterStats = true

View file

@ -10,11 +10,13 @@ import com.pixelized.shared.lwa.model.AlteredCharacterSheetFactory
import com.pixelized.shared.lwa.model.campaign.Campaign import com.pixelized.shared.lwa.model.campaign.Campaign
import com.pixelized.shared.lwa.model.campaign.CampaignJsonV1.CharacterInstanceJsonV1.CharacteristicV1.Damage import com.pixelized.shared.lwa.model.campaign.CampaignJsonV1.CharacterInstanceJsonV1.CharacteristicV1.Damage
import com.pixelized.shared.lwa.model.campaign.CampaignJsonV1.CharacterInstanceJsonV1.CharacteristicV1.Power import com.pixelized.shared.lwa.model.campaign.CampaignJsonV1.CharacterInstanceJsonV1.CharacteristicV1.Power
import com.pixelized.shared.lwa.protocol.websocket.Message import com.pixelized.shared.lwa.protocol.websocket.SocketMessage
import com.pixelized.shared.lwa.protocol.websocket.payload.CampaignMessage import com.pixelized.shared.lwa.protocol.websocket.CampaignMessage
import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation import com.pixelized.shared.lwa.protocol.websocket.GameMasterEvent
import com.pixelized.shared.lwa.protocol.websocket.payload.RollMessage import com.pixelized.shared.lwa.protocol.websocket.RestSynchronisation
import com.pixelized.shared.lwa.protocol.websocket.payload.UpdateSkillUsageMessage import com.pixelized.shared.lwa.protocol.websocket.RollMessage
import com.pixelized.shared.lwa.protocol.websocket.ToggleActiveAlteration
import com.pixelized.shared.lwa.protocol.websocket.UpdateSkillUsageMessage
import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.chat__characteristic__hp import lwacharactersheet.composeapp.generated.resources.chat__characteristic__hp
import lwacharactersheet.composeapp.generated.resources.chat__characteristic__pp import lwacharactersheet.composeapp.generated.resources.chat__characteristic__pp
@ -30,27 +32,27 @@ class TextMessageFactory(
private val formatTime = SimpleDateFormat("HH:mm:ss") private val formatTime = SimpleDateFormat("HH:mm:ss")
suspend fun convertToTextMessage( suspend fun convertToTextMessage(
message: Message, message: SocketMessage,
): TextMessage? { ): TextMessage? {
val time = System.currentTimeMillis() val time = message.timestamp
val id = formatId.format(time) val id = formatId.format(time)
return when (val payload = message.value) { return when (message) {
is RollMessage -> { is RollMessage -> {
val sheetPreview = characterSheetRepository val sheetPreview = characterSheetRepository
.characterPreview(characterId = payload.characterSheetId) .characterPreview(characterId = message.characterSheetId)
?: return null ?: return null
RollTextMessageUio( RollTextMessageUio(
id = "${payload.id.rollId}-${payload.id.timestamp}", id = "${message.uuid}-${message.timestamp}",
timestamp = formatTime.format(time), timestamp = formatTime.format(time),
character = sheetPreview.name, character = sheetPreview.name,
skillLabel = payload.skillLabel, skillLabel = message.skillLabel,
rollDifficulty = payload.rollDifficulty, rollDifficulty = message.rollDifficulty,
rollValue = payload.rollValue, rollValue = message.rollValue,
rollSuccessLimit = payload.rollSuccessLimit, rollSuccessLimit = message.rollSuccessLimit,
resultLabel = payload.resultLabel, resultLabel = message.resultLabel,
resultType = when (payload.critical) { resultType = when (message.critical) {
RollMessage.Critical.CRITICAL_SUCCESS -> RollTextMessageUio.Critical.CRITICAL_SUCCESS RollMessage.Critical.CRITICAL_SUCCESS -> RollTextMessageUio.Critical.CRITICAL_SUCCESS
RollMessage.Critical.SPECIAL_SUCCESS -> RollTextMessageUio.Critical.SPECIAL_SUCCESS RollMessage.Critical.SPECIAL_SUCCESS -> RollTextMessageUio.Critical.SPECIAL_SUCCESS
RollMessage.Critical.SUCCESS -> RollTextMessageUio.Critical.SUCCESS RollMessage.Critical.SUCCESS -> RollTextMessageUio.Critical.SUCCESS
@ -63,25 +65,25 @@ class TextMessageFactory(
is CampaignMessage.UpdateDiminished -> { is CampaignMessage.UpdateDiminished -> {
val sheetPreview = characterSheetRepository val sheetPreview = characterSheetRepository
.characterPreview(characterId = payload.characterSheetId) .characterPreview(characterId = message.characterSheetId)
?: return null ?: return null
DiminishedTextMessageUio( DiminishedTextMessageUio(
id = "${message.from}-$id-Diminished", id = "${message.timestamp}-$id-Diminished",
timestamp = formatTime.format(time), timestamp = formatTime.format(time),
character = sheetPreview.name, character = sheetPreview.name,
diminished = payload.diminished, diminished = message.diminished,
) )
} }
is CampaignMessage.UpdateCharacteristic -> { is CampaignMessage.UpdateCharacteristic -> {
val sheet = characterSheetRepository.characterDetail( val sheet = characterSheetRepository.characterDetail(
characterSheetId = payload.characterSheetId, characterSheetId = message.characterSheetId,
) ?: return null ) ?: return null
val characterInstanceId = Campaign.CharacterInstance.Id( val characterInstanceId = Campaign.CharacterInstance.Id(
prefix = payload.prefix, prefix = message.prefix,
characterSheetId = payload.characterSheetId, characterSheetId = message.characterSheetId,
instanceId = payload.instanceId, instanceId = message.instanceId,
) )
val alterations = alterationRepository.alterations( val alterations = alterationRepository.alterations(
characterInstanceId = characterInstanceId, characterInstanceId = characterInstanceId,
@ -91,25 +93,26 @@ class TextMessageFactory(
alterations = alterations, alterations = alterations,
) )
CharacteristicTextMessageUio( CharacteristicTextMessageUio(
id = "${message.from}-$id-Characteristic", id = "${message.timestamp}-$id-Characteristic",
timestamp = formatTime.format(time), timestamp = formatTime.format(time),
character = sheet.name, character = sheet.name,
value = when (payload.characteristic) { value = when (message.characteristic) {
Damage -> alteredSheet.maxHp - payload.value Damage -> alteredSheet.maxHp - message.value
Power -> alteredSheet.maxPp - payload.value Power -> alteredSheet.maxPp - message.value
}, },
characteristic = when (payload.characteristic) { characteristic = when (message.characteristic) {
Damage -> getString(Res.string.chat__characteristic__hp) Damage -> getString(Res.string.chat__characteristic__hp)
Power -> getString(Res.string.chat__characteristic__pp) Power -> getString(Res.string.chat__characteristic__pp)
}, },
) )
} }
RestSynchronisation.Campaign -> null is RestSynchronisation.Campaign -> null
is RestSynchronisation.CharacterDelete -> null is RestSynchronisation.CharacterSheetDelete -> null
is RestSynchronisation.CharacterUpdate -> null is RestSynchronisation.CharacterSheetUpdate -> null
is RestSynchronisation.ToggleActiveAlteration -> null is ToggleActiveAlteration -> null
is UpdateSkillUsageMessage -> null is UpdateSkillUsageMessage -> null
is GameMasterEvent -> null
} }
} }
} }

View file

@ -14,8 +14,8 @@ import com.pixelized.desktop.lwa.repository.network.NetworkRepository
import com.pixelized.desktop.lwa.ui.navigation.screen.destination.CharacterSheetDestination import com.pixelized.desktop.lwa.ui.navigation.screen.destination.CharacterSheetDestination
import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.dialog.CharacterSheetDeleteConfirmationDialogUio import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.dialog.CharacterSheetDeleteConfirmationDialogUio
import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.dialog.DiminishedStatDialogUio import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.dialog.DiminishedStatDialogUio
import com.pixelized.shared.lwa.protocol.websocket.payload.CampaignMessage import com.pixelized.shared.lwa.protocol.websocket.CampaignMessage
import com.pixelized.shared.lwa.protocol.websocket.payload.UpdateSkillUsageMessage import com.pixelized.shared.lwa.protocol.websocket.UpdateSkillUsageMessage
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
@ -80,7 +80,8 @@ class CharacterSheetViewModel(
fun onUseSkill(skill: CharacterSheetPageUio.Node) { fun onUseSkill(skill: CharacterSheetPageUio.Node) {
viewModelScope.launch { viewModelScope.launch {
network.share( network.share(
payload = UpdateSkillUsageMessage( message = UpdateSkillUsageMessage(
timestamp = System.currentTimeMillis(),
characterSheetId = argument.characterInstanceId.characterSheetId, characterSheetId = argument.characterInstanceId.characterSheetId,
skillId = skill.id, skillId = skill.id,
used = skill.used.not(), used = skill.used.not(),
@ -143,7 +144,8 @@ class CharacterSheetViewModel(
suspend fun changeDiminished(dialog: DiminishedStatDialogUio) { suspend fun changeDiminished(dialog: DiminishedStatDialogUio) {
val diminished = dialog.value().text.toIntOrNull() ?: 0 val diminished = dialog.value().text.toIntOrNull() ?: 0
network.share( network.share(
payload = CampaignMessage.UpdateDiminished( message = CampaignMessage.UpdateDiminished(
timestamp = System.currentTimeMillis(),
prefix = dialog.characterInstanceId.prefix, prefix = dialog.characterInstanceId.prefix,
characterSheetId = dialog.characterInstanceId.characterSheetId, characterSheetId = dialog.characterInstanceId.characterSheetId,
instanceId = dialog.characterInstanceId.instanceId, instanceId = dialog.characterInstanceId.instanceId,

View file

@ -29,9 +29,7 @@ import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold import androidx.compose.material.Scaffold
import androidx.compose.material.Surface import androidx.compose.material.Surface
import androidx.compose.material.Switch import androidx.compose.material.Switch
import androidx.compose.material.SwitchColors
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material.TextFieldDefaults
import androidx.compose.material.TopAppBar import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
@ -43,7 +41,6 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.LocalWindowController import com.pixelized.desktop.lwa.LocalWindowController
@ -63,6 +60,8 @@ import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__ed
import lwacharactersheet.composeapp.generated.resources.game_master__action import lwacharactersheet.composeapp.generated.resources.game_master__action
import lwacharactersheet.composeapp.generated.resources.game_master__title import lwacharactersheet.composeapp.generated.resources.game_master__title
import lwacharactersheet.composeapp.generated.resources.ic_cancel_24dp import lwacharactersheet.composeapp.generated.resources.ic_cancel_24dp
import lwacharactersheet.composeapp.generated.resources.ic_visibility_24dp
import lwacharactersheet.composeapp.generated.resources.ic_visibility_off_24dp
import org.jetbrains.compose.resources.getString import org.jetbrains.compose.resources.getString
import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
@ -77,6 +76,7 @@ fun GameMasterScreen(
val characters = viewModel.characters.collectAsState() val characters = viewModel.characters.collectAsState()
val gameMaster = viewModel.gameMaster.collectAsState() val gameMaster = viewModel.gameMaster.collectAsState()
val npcVisibility = viewModel.npcVisibility.collectAsState()
val tags = viewModel.tags.collectAsState() val tags = viewModel.tags.collectAsState()
Surface( Surface(
@ -87,6 +87,7 @@ fun GameMasterScreen(
filter = viewModel.filter, filter = viewModel.filter,
tags = tags, tags = tags,
gameMaster = gameMaster, gameMaster = gameMaster,
npcVisibility = npcVisibility,
characters = characters, characters = characters,
onTag = viewModel::onTag, onTag = viewModel::onTag,
onGameMaster = viewModel::onGameMaster, onGameMaster = viewModel::onGameMaster,
@ -107,6 +108,11 @@ fun GameMasterScreen(
) )
} }
}, },
onNpcVisibility = {
scope.launch {
viewModel.onNpcVisibility()
}
},
) )
} }
} }
@ -120,16 +126,18 @@ private fun GameMasterContent(
filter: LwaTextFieldUio, filter: LwaTextFieldUio,
tags: State<List<GMTagUio>>, tags: State<List<GMTagUio>>,
gameMaster: State<Boolean>, gameMaster: State<Boolean>,
npcVisibility: State<Boolean>,
characters: State<List<GMCharacterUio>>, characters: State<List<GMCharacterUio>>,
onGameMaster: (Boolean) -> Unit, onGameMaster: (Boolean) -> Unit,
onTag: (GMTagUio.TagId) -> Unit, onTag: (GMTagUio.TagId) -> Unit,
onCharacterAction: (String, GMCharacterUio.Action) -> Unit, onCharacterAction: (String, GMCharacterUio.Action) -> Unit,
onCharacterSheetEdit: (String) -> Unit, onCharacterSheetEdit: (String) -> Unit,
onCharacterSheetCreate: () -> Unit, onCharacterSheetCreate: () -> Unit,
onNpcVisibility: () -> Unit,
) { ) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
Scaffold( GameMasterLayout(
modifier = modifier, modifier = modifier,
topBar = { topBar = {
TopAppBar( TopAppBar(
@ -161,10 +169,8 @@ private fun GameMasterContent(
} }
) )
}, },
content = { paddingValues -> content = {
Column( Column {
modifier = Modifier.padding(paddingValues = paddingValues)
) {
Surface( Surface(
elevation = 1.dp, elevation = 1.dp,
) { ) {
@ -210,7 +216,6 @@ private fun GameMasterContent(
items = tags.value, items = tags.value,
) { tag -> ) { tag ->
GMTag( GMTag(
style = MaterialTheme.lwa.typography.base.caption,
tag = tag, tag = tag,
onTag = { onTag(tag.id) }, onTag = { onTag(tag.id) },
) )
@ -219,7 +224,9 @@ private fun GameMasterContent(
} }
} }
Box( Box(
modifier = Modifier.fillMaxWidth().weight(1f), modifier = Modifier
.fillMaxWidth()
.weight(1f),
) { ) {
LazyColumn( LazyColumn(
modifier = Modifier.matchParentSize(), modifier = Modifier.matchParentSize(),
@ -251,22 +258,71 @@ private fun GameMasterContent(
) )
} }
} }
IconButton( }
modifier = Modifier }
.align(alignment = Alignment.BottomEnd) },
.padding(all = padding) fab = {
.background( Row(
color = MaterialTheme.lwa.colorScheme.base.primary, modifier = Modifier
shape = CircleShape, .fillMaxWidth()
), .padding(all = padding),
onClick = onCharacterSheetCreate, horizontalArrangement = Arrangement.SpaceBetween,
) { ) {
Icon( IconButton(
imageVector = Icons.Default.Add, modifier = Modifier.background(
tint = MaterialTheme.lwa.colorScheme.base.onPrimary, color = MaterialTheme.lwa.colorScheme.base.primary,
contentDescription = null, shape = CircleShape,
) ),
} onClick = onNpcVisibility,
) {
Icon(
painter = when (npcVisibility.value) {
true -> painterResource(Res.drawable.ic_visibility_off_24dp)
else -> painterResource(Res.drawable.ic_visibility_24dp)
},
tint = MaterialTheme.lwa.colorScheme.base.onPrimary,
contentDescription = null,
)
}
IconButton(
modifier = Modifier.background(
color = MaterialTheme.lwa.colorScheme.base.primary,
shape = CircleShape,
),
onClick = onCharacterSheetCreate,
) {
Icon(
imageVector = Icons.Default.Add,
tint = MaterialTheme.lwa.colorScheme.base.onPrimary,
contentDescription = null,
)
}
}
}
)
}
@Composable
private fun GameMasterLayout(
modifier: Modifier,
topBar: @Composable () -> Unit,
content: @Composable () -> Unit,
fab: @Composable () -> Unit,
) {
Scaffold(
modifier = modifier,
topBar = topBar,
content = { paddingValues ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues = paddingValues)
) {
content()
Row(
modifier = Modifier.align(alignment = Alignment.BottomStart),
) {
fab()
} }
} }
} }

View file

@ -4,11 +4,13 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.desktop.lwa.repository.network.NetworkRepository
import com.pixelized.desktop.lwa.repository.settings.SettingsRepository import com.pixelized.desktop.lwa.repository.settings.SettingsRepository
import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextFieldUio import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextFieldUio
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMCharacterUio import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMCharacterUio
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMTagUio import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMTagUio
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMTagUio.TagId import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMTagUio.TagId
import com.pixelized.shared.lwa.protocol.websocket.GameMasterEvent
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
@ -26,6 +28,7 @@ class GameMasterViewModel(
campaignRepository: CampaignRepository, campaignRepository: CampaignRepository,
characterSheetRepository: CharacterSheetRepository, characterSheetRepository: CharacterSheetRepository,
private val settingsRepository: SettingsRepository, private val settingsRepository: SettingsRepository,
private val networkRepository: NetworkRepository,
private val factory: GameMasterFactory, private val factory: GameMasterFactory,
private val useCase: GameMasterActionUseCase, private val useCase: GameMasterActionUseCase,
) : ViewModel() { ) : ViewModel() {
@ -82,6 +85,14 @@ class GameMasterViewModel(
initialValue = false, initialValue = false,
) )
val npcVisibility = campaignRepository.campaignFlow
.map { it.options.showNpcs }
.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = false,
)
fun onGameMaster(value: Boolean) { fun onGameMaster(value: Boolean) {
val settings = settingsRepository.settings() val settings = settingsRepository.settings()
settingsRepository.update( settingsRepository.update(
@ -114,4 +125,12 @@ class GameMasterViewModel(
it[id] = it.getOrPut(id) { true }.not() it[id] = it.getOrPut(id) { true }.not()
} }
} }
suspend fun onNpcVisibility() {
networkRepository.share(
GameMasterEvent.ToggleNpc(
timestamp = System.currentTimeMillis(),
)
)
}
} }

View file

@ -144,7 +144,6 @@ fun GMCharacter(
) { ) {
character.tags.forEach { tag -> character.tags.forEach { tag ->
GMTag( GMTag(
style = MaterialTheme.lwa.typography.base.caption,
elevation = 4.dp, elevation = 4.dp,
tag = tag, tag = tag,
) )

View file

@ -41,7 +41,6 @@ fun GMTag(
padding: PaddingValues = GmTagDefault.padding, padding: PaddingValues = GmTagDefault.padding,
shape: Shape = CircleShape, shape: Shape = CircleShape,
elevation: Dp = 2.dp, elevation: Dp = 2.dp,
style: TextStyle,
tag: GMTagUio, tag: GMTagUio,
onTag: (() -> Unit)? = null, onTag: (() -> Unit)? = null,
) { ) {
@ -60,7 +59,7 @@ fun GMTag(
modifier = Modifier modifier = Modifier
.clickable(enabled = onTag != null) { onTag?.invoke() } .clickable(enabled = onTag != null) { onTag?.invoke() }
.padding(paddingValues = padding), .padding(paddingValues = padding),
style = style, style = MaterialTheme.lwa.typography.base.caption,
color = animatedColor.value, color = animatedColor.value,
text = tag.label, text = tag.label,
) )

View file

@ -209,4 +209,24 @@ class CampaignService(
} }
} }
} }
suspend fun updateToggleParty() {
store.save(
campaign = campaign.copy(
options = campaign.options.copy(
showParty = campaign.options.showParty.not()
)
)
)
}
suspend fun updateToggleNpc() {
store.save(
campaign = campaign.copy(
options = campaign.options.copy(
showNpcs = campaign.options.showNpcs.not()
)
)
)
}
} }

View file

@ -5,11 +5,13 @@ import com.pixelized.server.lwa.model.campaign.CampaignService
import com.pixelized.server.lwa.model.character.CharacterSheetService import com.pixelized.server.lwa.model.character.CharacterSheetService
import com.pixelized.shared.lwa.model.campaign.Campaign import com.pixelized.shared.lwa.model.campaign.Campaign
import com.pixelized.shared.lwa.model.campaign.factory.CampaignJsonFactory import com.pixelized.shared.lwa.model.campaign.factory.CampaignJsonFactory
import com.pixelized.shared.lwa.protocol.websocket.Message import com.pixelized.shared.lwa.protocol.websocket.SocketMessage
import com.pixelized.shared.lwa.protocol.websocket.payload.CampaignMessage import com.pixelized.shared.lwa.protocol.websocket.CampaignMessage
import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation import com.pixelized.shared.lwa.protocol.websocket.GameMasterEvent
import com.pixelized.shared.lwa.protocol.websocket.payload.RollMessage import com.pixelized.shared.lwa.protocol.websocket.RestSynchronisation
import com.pixelized.shared.lwa.protocol.websocket.payload.UpdateSkillUsageMessage import com.pixelized.shared.lwa.protocol.websocket.RollMessage
import com.pixelized.shared.lwa.protocol.websocket.ToggleActiveAlteration
import com.pixelized.shared.lwa.protocol.websocket.UpdateSkillUsageMessage
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
class Engine( class Engine(
@ -18,46 +20,51 @@ class Engine(
val alterationService: AlterationService, val alterationService: AlterationService,
val campaignJsonFactory: CampaignJsonFactory, val campaignJsonFactory: CampaignJsonFactory,
) { ) {
val webSocket = MutableSharedFlow<Message>() val webSocket = MutableSharedFlow<SocketMessage>()
suspend fun handle(message: Message) { suspend fun handle(message: SocketMessage) {
when (val data = message.value) { when (message) {
is RollMessage -> Unit // Nothing to do here. is RollMessage -> Unit // Nothing to do here.
is CampaignMessage -> { is CampaignMessage -> {
val instanceId = Campaign.CharacterInstance.Id( val instanceId = Campaign.CharacterInstance.Id(
prefix = data.prefix, prefix = message.prefix,
characterSheetId = data.characterSheetId, characterSheetId = message.characterSheetId,
instanceId = data.instanceId, instanceId = message.instanceId,
) )
when (data) { when (message) {
is CampaignMessage.UpdateCharacteristic -> campaignService.updateCharacteristic( is CampaignMessage.UpdateCharacteristic -> campaignService.updateCharacteristic(
characterInstanceId = instanceId, characterInstanceId = instanceId,
characteristic = campaignJsonFactory.convertFromJson(json = data.characteristic), characteristic = campaignJsonFactory.convertFromJson(json = message.characteristic),
value = data.value, value = message.value,
) )
is CampaignMessage.UpdateDiminished -> campaignService.updateDiminished( is CampaignMessage.UpdateDiminished -> campaignService.updateDiminished(
characterInstanceId = instanceId, characterInstanceId = instanceId,
diminished = data.diminished, diminished = message.diminished,
) )
} }
} }
is UpdateSkillUsageMessage -> characterService.updateCharacterSkillUsage( is UpdateSkillUsageMessage -> characterService.updateCharacterSkillUsage(
characterSheetId = data.characterSheetId, characterSheetId = message.characterSheetId,
skillId = data.skillId, skillId = message.skillId,
used = data.used, used = message.used,
) )
RestSynchronisation.Campaign -> Unit // Handle in the Rest is RestSynchronisation.Campaign -> Unit // Handle in the Rest
is RestSynchronisation.CharacterUpdate -> Unit // Handle in the Rest is RestSynchronisation.CharacterSheetUpdate -> Unit // Handle in the Rest
is RestSynchronisation.CharacterDelete -> Unit // Handle in the Rest is RestSynchronisation.CharacterSheetDelete -> Unit // Handle in the Rest
is RestSynchronisation.ToggleActiveAlteration -> Unit // Handle in the Rest is ToggleActiveAlteration -> Unit // Handle in the Rest
is GameMasterEvent -> when (message) {
is GameMasterEvent.TogglePlayer -> campaignService.updateToggleParty()
is GameMasterEvent.ToggleNpc -> campaignService.updateToggleNpc()
}
} }
} }
} }

View file

@ -15,7 +15,7 @@ import com.pixelized.server.lwa.server.rest.character.getCharacter
import com.pixelized.server.lwa.server.rest.character.getCharacters import com.pixelized.server.lwa.server.rest.character.getCharacters
import com.pixelized.server.lwa.server.rest.character.putCharacter import com.pixelized.server.lwa.server.rest.character.putCharacter
import com.pixelized.shared.lwa.SERVER_PORT import com.pixelized.shared.lwa.SERVER_PORT
import com.pixelized.shared.lwa.protocol.websocket.Message import com.pixelized.shared.lwa.protocol.websocket.SocketMessage
import com.pixelized.shared.lwa.sharedModuleDependencies import com.pixelized.shared.lwa.sharedModuleDependencies
import io.ktor.serialization.kotlinx.json.json import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.install import io.ktor.server.application.install
@ -97,7 +97,7 @@ class LocalServer {
incoming.consumeEach { frame -> incoming.consumeEach { frame ->
if (frame is Frame.Text) { if (frame is Frame.Text) {
val data = frame.readText() val data = frame.readText()
val message = json.decodeFromString<Message>(data) val message = json.decodeFromString<SocketMessage>(data)
// log the message // log the message
engine.handle(message) engine.handle(message)
// broadcast to clients the message // broadcast to clients the message

View file

@ -2,8 +2,7 @@ package com.pixelized.server.lwa.server.rest.alteration
import com.pixelized.server.lwa.server.Engine import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.characterInstanceId import com.pixelized.server.lwa.utils.extentions.characterInstanceId
import com.pixelized.shared.lwa.protocol.websocket.Message import com.pixelized.shared.lwa.protocol.websocket.ToggleActiveAlteration
import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.server.request.receive import io.ktor.server.request.receive
import io.ktor.server.response.respondText import io.ktor.server.response.respondText
@ -32,15 +31,15 @@ fun Engine.putActiveAlteration(): suspend io.ktor.server.routing.RoutingContext.
) )
// share the modification to all client through the websocket. // share the modification to all client through the websocket.
webSocket.emit( webSocket.emit(
Message( ToggleActiveAlteration(
from = "Server", timestamp = System.currentTimeMillis(),
value = RestSynchronisation.ToggleActiveAlteration( prefix = characterInstanceId.prefix,
characterId = campaignJsonFactory.convertToJson(id = characterInstanceId), characterSheetId = characterInstanceId.characterSheetId,
alterationId = alterationId, instanceId = characterInstanceId.instanceId,
active = alterationService.isAlterationActive( alterationId = alterationId,
characterInstanceId = characterInstanceId, active = alterationService.isAlterationActive(
alterationId = alterationId characterInstanceId = characterInstanceId,
), alterationId = alterationId
), ),
) )
) )

View file

@ -2,8 +2,7 @@ package com.pixelized.server.lwa.server.rest.campaign
import com.pixelized.server.lwa.server.Engine import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.characterInstanceId import com.pixelized.server.lwa.utils.extentions.characterInstanceId
import com.pixelized.shared.lwa.protocol.websocket.Message import com.pixelized.shared.lwa.protocol.websocket.RestSynchronisation
import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.server.response.respondText import io.ktor.server.response.respondText
@ -24,9 +23,8 @@ fun Engine.removeCampaignCharacter(): suspend io.ktor.server.routing.RoutingCont
status = HttpStatusCode.Accepted, status = HttpStatusCode.Accepted,
) )
webSocket.emit( webSocket.emit(
Message( RestSynchronisation.Campaign(
from = "Server", timestamp = System.currentTimeMillis(),
value = RestSynchronisation.Campaign,
) )
) )
} catch (exception: Exception) { } catch (exception: Exception) {

View file

@ -2,8 +2,7 @@ package com.pixelized.server.lwa.server.rest.campaign
import com.pixelized.server.lwa.server.Engine import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.characterInstanceId import com.pixelized.server.lwa.utils.extentions.characterInstanceId
import com.pixelized.shared.lwa.protocol.websocket.Message import com.pixelized.shared.lwa.protocol.websocket.RestSynchronisation
import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.server.response.respondText import io.ktor.server.response.respondText
@ -24,9 +23,8 @@ fun Engine.deleteCampaignNpc(): suspend io.ktor.server.routing.RoutingContext.()
status = HttpStatusCode.Accepted, status = HttpStatusCode.Accepted,
) )
webSocket.emit( webSocket.emit(
Message( RestSynchronisation.Campaign(
from = "Server", timestamp = System.currentTimeMillis(),
value = RestSynchronisation.Campaign,
) )
) )
} catch (exception: Exception) { } catch (exception: Exception) {

View file

@ -1,11 +1,8 @@
package com.pixelized.server.lwa.server.rest.campaign package com.pixelized.server.lwa.server.rest.campaign
import com.pixelized.server.lwa.server.Engine import com.pixelized.server.lwa.server.Engine
import com.pixelized.shared.lwa.model.campaign.Campaign
import com.pixelized.shared.lwa.model.campaign.CampaignJsonV1 import com.pixelized.shared.lwa.model.campaign.CampaignJsonV1
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJson import com.pixelized.shared.lwa.protocol.websocket.RestSynchronisation
import com.pixelized.shared.lwa.protocol.websocket.Message
import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.server.request.receive import io.ktor.server.request.receive
import io.ktor.server.response.respondText import io.ktor.server.response.respondText
@ -29,9 +26,8 @@ fun Engine.putCampaignScene(): suspend io.ktor.server.routing.RoutingContext.()
) )
webSocket.emit( webSocket.emit(
Message( RestSynchronisation.Campaign(
from = "Server", timestamp = System.currentTimeMillis(),
value = RestSynchronisation.Campaign,
) )
) )
} }

View file

@ -3,8 +3,7 @@ package com.pixelized.server.lwa.server.rest.campaign
import com.pixelized.server.lwa.server.Engine import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.characterSheetId import com.pixelized.server.lwa.utils.extentions.characterSheetId
import com.pixelized.shared.lwa.model.campaign.Campaign import com.pixelized.shared.lwa.model.campaign.Campaign
import com.pixelized.shared.lwa.protocol.websocket.Message import com.pixelized.shared.lwa.protocol.websocket.RestSynchronisation
import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.server.response.respondText import io.ktor.server.response.respondText
@ -36,9 +35,8 @@ fun Engine.putCampaignCharacter(): suspend io.ktor.server.routing.RoutingContext
status = HttpStatusCode.Accepted, status = HttpStatusCode.Accepted,
) )
webSocket.emit( webSocket.emit(
Message( RestSynchronisation.Campaign(
from = "Server", timestamp = System.currentTimeMillis(),
value = RestSynchronisation.Campaign,
) )
) )
} catch (exception: Exception) { } catch (exception: Exception) {

View file

@ -3,8 +3,7 @@ package com.pixelized.server.lwa.server.rest.campaign
import com.pixelized.server.lwa.server.Engine import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.characterSheetId import com.pixelized.server.lwa.utils.extentions.characterSheetId
import com.pixelized.shared.lwa.model.campaign.Campaign import com.pixelized.shared.lwa.model.campaign.Campaign
import com.pixelized.shared.lwa.protocol.websocket.Message import com.pixelized.shared.lwa.protocol.websocket.RestSynchronisation
import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.server.response.respondText import io.ktor.server.response.respondText
@ -39,9 +38,8 @@ fun Engine.putCampaignNpc(): suspend io.ktor.server.routing.RoutingContext.() ->
status = HttpStatusCode.Accepted, status = HttpStatusCode.Accepted,
) )
webSocket.emit( webSocket.emit(
Message( RestSynchronisation.Campaign(
from = "Server", timestamp = System.currentTimeMillis(),
value = RestSynchronisation.Campaign,
) )
) )
} catch (exception: Exception) { } catch (exception: Exception) {

View file

@ -2,8 +2,7 @@ package com.pixelized.server.lwa.server.rest.character
import com.pixelized.server.lwa.server.Engine import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.characterSheetId import com.pixelized.server.lwa.utils.extentions.characterSheetId
import com.pixelized.shared.lwa.protocol.websocket.Message import com.pixelized.shared.lwa.protocol.websocket.RestSynchronisation
import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.server.response.respondText import io.ktor.server.response.respondText
@ -18,10 +17,10 @@ fun Engine.deleteCharacter(): suspend io.ktor.server.routing.RoutingContext.() -
status = HttpStatusCode.OK, status = HttpStatusCode.OK,
) )
webSocket.emit( webSocket.emit(
Message( RestSynchronisation.CharacterSheetDelete(
from = "Server", timestamp = System.currentTimeMillis(),
value = RestSynchronisation.CharacterDelete(characterId = characterSheetId), characterSheetId = characterSheetId,
) ),
) )
} else { } else {
call.respondText( call.respondText(

View file

@ -2,8 +2,7 @@ package com.pixelized.server.lwa.server.rest.character
import com.pixelized.server.lwa.server.Engine import com.pixelized.server.lwa.server.Engine
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJson import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJson
import com.pixelized.shared.lwa.protocol.websocket.Message import com.pixelized.shared.lwa.protocol.websocket.RestSynchronisation
import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.server.request.receive import io.ktor.server.request.receive
import io.ktor.server.response.respondText import io.ktor.server.response.respondText
@ -19,10 +18,10 @@ fun Engine.putCharacter(): suspend io.ktor.server.routing.RoutingContext.() -> U
status = HttpStatusCode.OK status = HttpStatusCode.OK
) )
webSocket.emit( webSocket.emit(
Message( RestSynchronisation.CharacterSheetUpdate(
from = "Server", timestamp = System.currentTimeMillis(),
value = RestSynchronisation.CharacterUpdate(id = form.id), characterSheetId = form.id,
) ),
) )
} }
} }

View file

@ -4,6 +4,7 @@ data class Campaign(
val characters: Map<CharacterInstance.Id, CharacterInstance>, val characters: Map<CharacterInstance.Id, CharacterInstance>,
val npcs: Map<CharacterInstance.Id, CharacterInstance>, val npcs: Map<CharacterInstance.Id, CharacterInstance>,
val scene: Scene, val scene: Scene,
val options: Options,
) { ) {
data class CharacterInstance( data class CharacterInstance(
val characteristic: Map<Characteristic, Int>, val characteristic: Map<Characteristic, Int>,
@ -53,11 +54,24 @@ data class Campaign(
} }
} }
data class Options(
val showParty: Boolean,
val showNpcs: Boolean,
) {
companion object {
fun empty() = Options(
showParty = true,
showNpcs = false,
)
}
}
companion object { companion object {
val EMPTY = Campaign( val EMPTY = Campaign(
characters = emptyMap(), characters = emptyMap(),
npcs = emptyMap(), npcs = emptyMap(),
scene = Scene.empty(), scene = Scene.empty(),
options = Options.empty(),
) )
} }
} }

View file

@ -14,4 +14,7 @@ sealed interface CampaignJson {
@Serializable @Serializable
sealed interface SceneJson sealed interface SceneJson
@Serializable
sealed interface OptionJson
} }

View file

@ -7,6 +7,7 @@ data class CampaignJsonV1(
val characters: Map<String, CharacterInstanceJsonV1>, val characters: Map<String, CharacterInstanceJsonV1>,
val npcs: Map<String, CharacterInstanceJsonV1>, val npcs: Map<String, CharacterInstanceJsonV1>,
val scene: SceneJsonV1?, val scene: SceneJsonV1?,
val options: OptionsJsonV1?,
) : CampaignJson { ) : CampaignJson {
@Serializable @Serializable
@ -24,4 +25,10 @@ data class CampaignJsonV1(
data class SceneJsonV1( data class SceneJsonV1(
val name: String, val name: String,
) : CampaignJson.SceneJson ) : CampaignJson.SceneJson
@Serializable
data class OptionsJsonV1(
val showPlayer: Boolean,
val showNpcs: Boolean,
) : CampaignJson.OptionJson
} }

View file

@ -58,6 +58,10 @@ class CampaignJsonFactory(
.toMap(), .toMap(),
scene = CampaignJsonV1.SceneJsonV1( scene = CampaignJsonV1.SceneJsonV1(
name = data.scene.name name = data.scene.name
),
options = CampaignJsonV1.OptionsJsonV1(
showPlayer = data.options.showParty,
showNpcs = data.options.showNpcs,
) )
) )
} }

View file

@ -26,6 +26,9 @@ class CampaignJsonV1Factory {
scene = campaignJson.scene scene = campaignJson.scene
?.let { convertFromV1(it) } ?.let { convertFromV1(it) }
?: Campaign.Scene.empty(), ?: Campaign.Scene.empty(),
options = campaignJson.options
?.let { convertFromV1(it) }
?:Campaign.Options.empty()
) )
} }
@ -66,4 +69,13 @@ class CampaignJsonV1Factory {
name = sceneJson.name name = sceneJson.name
) )
} }
fun convertFromV1(
optionsJson: CampaignJsonV1.OptionsJsonV1,
): Campaign.Options {
return Campaign.Options(
showParty = optionsJson.showPlayer,
showNpcs = optionsJson.showNpcs
)
}
} }

View file

@ -1,16 +1,17 @@
package com.pixelized.shared.lwa.protocol.websocket.payload package com.pixelized.shared.lwa.protocol.websocket
import com.pixelized.shared.lwa.model.campaign.CampaignJsonV1 import com.pixelized.shared.lwa.model.campaign.CampaignJsonV1
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
sealed interface CampaignMessage : MessagePayload { sealed interface CampaignMessage : SocketMessage, CharacterInstanceIdMessage {
val prefix: Char override val prefix: Char
val characterSheetId: String override val characterSheetId: String
val instanceId: Int override val instanceId: Int
@Serializable @Serializable
data class UpdateCharacteristic( data class UpdateCharacteristic(
override val timestamp: Long,
override val prefix: Char, override val prefix: Char,
override val characterSheetId: String, override val characterSheetId: String,
override val instanceId: Int, override val instanceId: Int,
@ -20,6 +21,7 @@ sealed interface CampaignMessage : MessagePayload {
@Serializable @Serializable
data class UpdateDiminished( data class UpdateDiminished(
override val timestamp: Long,
override val prefix: Char, override val prefix: Char,
override val characterSheetId: String, override val characterSheetId: String,
override val instanceId: Int, override val instanceId: Int,

View file

@ -0,0 +1,15 @@
package com.pixelized.shared.lwa.protocol.websocket
import kotlinx.serialization.Serializable
@Serializable
sealed interface CharacterSheetIdMessage {
val characterSheetId: String
}
@Serializable
sealed interface CharacterInstanceIdMessage : CharacterSheetIdMessage {
override val characterSheetId: String
val prefix: Char
val instanceId: Int
}

View file

@ -0,0 +1,17 @@
package com.pixelized.shared.lwa.protocol.websocket
import kotlinx.serialization.Serializable
@Serializable
sealed interface GameMasterEvent : SocketMessage {
@Serializable
class ToggleNpc(
override val timestamp: Long,
) : GameMasterEvent
@Serializable
class TogglePlayer(
override val timestamp: Long,
) : GameMasterEvent
}

View file

@ -1,10 +0,0 @@
package com.pixelized.shared.lwa.protocol.websocket
import com.pixelized.shared.lwa.protocol.websocket.payload.MessagePayload
import kotlinx.serialization.Serializable
@Serializable
data class Message(
val from: String,
val value: MessagePayload,
)

View file

@ -0,0 +1,24 @@
package com.pixelized.shared.lwa.protocol.websocket
import kotlinx.serialization.Serializable
@Serializable
sealed class RestSynchronisation : SocketMessage {
@Serializable
data class CharacterSheetUpdate(
override val timestamp: Long,
override val characterSheetId: String,
) : RestSynchronisation(), CharacterSheetIdMessage
@Serializable
data class CharacterSheetDelete(
override val timestamp: Long,
override val characterSheetId: String,
) : RestSynchronisation(), CharacterSheetIdMessage
@Serializable
data class Campaign(
override val timestamp: Long,
) : RestSynchronisation()
}

View file

@ -0,0 +1,26 @@
package com.pixelized.shared.lwa.protocol.websocket
import kotlinx.serialization.Serializable
@Serializable
data class RollMessage(
override val timestamp: Long,
val uuid: String,
val prefix: Char,
val characterSheetId: String,
val instanceId: Int?,
val skillLabel: String,
val rollValue: Int,
val resultLabel: String? = null,
val rollDifficulty: String? = null,
val rollSuccessLimit: Int? = null,
val critical: Critical? = null,
) : SocketMessage {
enum class Critical {
CRITICAL_SUCCESS,
SPECIAL_SUCCESS,
SUCCESS,
FAILURE,
CRITICAL_FAILURE
}
}

View file

@ -0,0 +1,8 @@
package com.pixelized.shared.lwa.protocol.websocket
import kotlinx.serialization.Serializable
@Serializable
sealed interface SocketMessage {
val timestamp: Long
}

View file

@ -0,0 +1,13 @@
package com.pixelized.shared.lwa.protocol.websocket
import kotlinx.serialization.Serializable
@Serializable
data class ToggleActiveAlteration(
override val timestamp: Long,
override val characterSheetId: String,
override val prefix: Char,
override val instanceId: Int,
val alterationId: String,
val active: Boolean,
) : SocketMessage, CharacterInstanceIdMessage

View file

@ -0,0 +1,11 @@
package com.pixelized.shared.lwa.protocol.websocket
import kotlinx.serialization.Serializable
@Serializable
data class UpdateSkillUsageMessage(
override val timestamp: Long,
override val characterSheetId: String,
val skillId: String,
val used: Boolean,
) : SocketMessage, CharacterSheetIdMessage

View file

@ -1,6 +0,0 @@
package com.pixelized.shared.lwa.protocol.websocket.payload
import kotlinx.serialization.Serializable
@Serializable
sealed interface MessagePayload

View file

@ -1,27 +0,0 @@
package com.pixelized.shared.lwa.protocol.websocket.payload
import kotlinx.serialization.Serializable
@Serializable
sealed class RestSynchronisation : MessagePayload {
@Serializable
data class CharacterUpdate(
val id: String,
) : RestSynchronisation()
@Serializable
data class CharacterDelete(
val characterId: String,
) : RestSynchronisation()
@Serializable
data class ToggleActiveAlteration(
val characterId: String,
val alterationId: String,
val active: Boolean,
) : RestSynchronisation()
@Serializable
data object Campaign : RestSynchronisation()
}

View file

@ -1,43 +0,0 @@
package com.pixelized.shared.lwa.protocol.websocket.payload
import kotlinx.serialization.Serializable
import java.util.UUID
@Serializable
data class RollMessage(
val id: RollId,
val prefix: Char,
val characterSheetId: String,
val instanceId: Int?,
val skillLabel: String,
val rollValue: Int,
val resultLabel: String? = null,
val rollDifficulty: String? = null,
val rollSuccessLimit: Int? = null,
val critical: Critical? = null,
) : MessagePayload {
@Serializable
data class RollId(
val rollId: String,
val timestamp: Long,
) {
companion object {
fun create(
rollId: String = UUID.randomUUID().toString(),
timestamp: Long = System.currentTimeMillis(),
) = RollId(
rollId = rollId,
timestamp = timestamp,
)
}
}
enum class Critical {
CRITICAL_SUCCESS,
SPECIAL_SUCCESS,
SUCCESS,
FAILURE,
CRITICAL_FAILURE
}
}

View file

@ -1,10 +0,0 @@
package com.pixelized.shared.lwa.protocol.websocket.payload
import kotlinx.serialization.Serializable
@Serializable
data class UpdateSkillUsageMessage(
val characterSheetId: String,
val skillId: String,
val used: Boolean,
) : MessagePayload