diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/App.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/App.kt index 313d952..1a5fdd1 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/App.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/App.kt @@ -52,7 +52,7 @@ import com.pixelized.desktop.lwa.ui.navigation.window.destination.RollHistoryWin import com.pixelized.desktop.lwa.ui.navigation.window.rememberMaxWindowHeight import com.pixelized.desktop.lwa.ui.overlay.roll.RollHostState import com.pixelized.desktop.lwa.ui.overlay.roll.RollOverlay -import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.PlayerRibbon +import com.pixelized.desktop.lwa.ui.screen.campaign.player.CharacterPortraitDefault import com.pixelized.desktop.lwa.ui.screen.characterSheet.CharacterSheetMainNavHost import com.pixelized.desktop.lwa.ui.screen.gamemaster.GameMasterScreen import com.pixelized.desktop.lwa.ui.screen.rollhistory.RollHistoryPage @@ -108,7 +108,7 @@ fun ApplicationScope.LwaApplication() { size = DpSize( width = 800.dp, height = min( - a = 56.dp + PlayerRibbon.Default.size.height * 6 + 8.dp * 7 + 40.dp, + a = 56.dp + CharacterPortraitDefault.size.height * 6 + 8.dp * 7 + 40.dp, b = maxWindowHeight, ), ), diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/DataSyncViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/DataSyncViewModel.kt index 4eaa5a8..5ef9b8f 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/DataSyncViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/DataSyncViewModel.kt @@ -43,7 +43,7 @@ class DataSyncViewModel( .filter { status -> status == NetworkRepository.Status.CONNECTED } .combine(campaignRepository.campaignFlow) { _, campaign: Campaign -> campaign } .onEach { campaign -> - campaign.characters.keys.forEach { id -> + (campaign.characters.keys + campaign.npcs.keys).forEach { id -> characterRepository.characterDetail( characterSheetId = id.characterSheetId, forceUpdate = true, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/Module.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/Module.kt index d49e879..07ad66d 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/Module.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/Module.kt @@ -17,13 +17,13 @@ import com.pixelized.desktop.lwa.ui.composable.character.characteristic.Characte import com.pixelized.desktop.lwa.ui.composable.character.characteristic.CharacterSheetCharacteristicDialogFactory import com.pixelized.desktop.lwa.ui.overlay.roll.RollViewModel import com.pixelized.desktop.lwa.ui.screen.campaign.toolbar.CampaignToolbarViewModel -import com.pixelized.desktop.lwa.ui.screen.campaign.chat.CampaignChatViewModel -import com.pixelized.desktop.lwa.ui.screen.campaign.chat.TextMessageFactory +import com.pixelized.desktop.lwa.ui.screen.campaign.text.CampaignChatViewModel +import com.pixelized.desktop.lwa.ui.screen.campaign.text.TextMessageFactory import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailFactory import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailViewModel import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDiminishedViewModel -import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.PlayerRibbonFactory -import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.PlayerRibbonViewModel +import com.pixelized.desktop.lwa.ui.screen.campaign.player.CharacterRibbonFactory +import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.player.PlayerRibbonViewModel import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.CharacterSheetFactory import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.CharacterSheetViewModel import com.pixelized.desktop.lwa.ui.screen.characterSheet.edit.CharacterSheetEditFactory @@ -33,6 +33,7 @@ import com.pixelized.desktop.lwa.ui.screen.levelup.LevelUpFactory import com.pixelized.desktop.lwa.ui.screen.levelup.LevelUpViewModel import com.pixelized.desktop.lwa.ui.screen.campaign.network.NetworkFactory import com.pixelized.desktop.lwa.ui.screen.campaign.network.NetworkViewModel +import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.npc.NpcRibbonViewModel import com.pixelized.desktop.lwa.ui.screen.gamemaster.GameMasterActionUseCase import com.pixelized.desktop.lwa.ui.screen.gamemaster.GameMasterFactory import com.pixelized.desktop.lwa.ui.screen.gamemaster.GameMasterViewModel @@ -109,7 +110,7 @@ val factoryDependencies factoryOf(::SkillFieldFactory) factoryOf(::SettingsFactory) factoryOf(::CampaignJsonFactory) - factoryOf(::PlayerRibbonFactory) + factoryOf(::CharacterRibbonFactory) factoryOf(::CharacterDetailFactory) factoryOf(::CharacterSheetCharacteristicDialogFactory) factoryOf(::TextMessageFactory) @@ -127,6 +128,7 @@ val viewModelDependencies viewModelOf(::RollHistoryViewModel) viewModelOf(::NetworkViewModel) viewModelOf(::PlayerRibbonViewModel) + viewModelOf(::NpcRibbonViewModel) viewModelOf(::CharacterDetailViewModel) viewModelOf(::CharacterDiminishedViewModel) viewModelOf(::CharacterDetailCharacteristicDialogViewModel) diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClient.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClient.kt index 173e37a..bb13fe6 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClient.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClient.kt @@ -13,23 +13,28 @@ interface LwaClient { suspend fun updateCharacter(sheet: CharacterSheetJson) - suspend fun deleteCharacter(id: String) + suspend fun deleteCharacterSheet(id: String) suspend fun campaign(): CampaignJson suspend fun campaignAddCharacter(characterSheetId: String) - suspend fun campaignDeleteCharacter(characterSheetId: String, instanceId: Int) + suspend fun campaignRemoveCharacter(characterSheetId: String, instanceId: Int) suspend fun campaignAddNpc(characterSheetId: String) - suspend fun campaignDeleteNpc(characterSheetId: String, instanceId: Int) + suspend fun campaignRemoveNpc(characterSheetId: String, instanceId: Int) suspend fun alterations(): List - suspend fun activeAlterations(characterSheetId: String, instanceId: Int): List + suspend fun activeAlterations( + prefix: Char, + characterSheetId: String, + instanceId: Int, + ): List suspend fun toggleActiveAlterations( + prefix: Char, characterSheetId: String, instanceId: Int, alterationId: String, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClientImpl.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClientImpl.kt index 19e2b3c..b1b2c69 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClientImpl.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClientImpl.kt @@ -2,6 +2,7 @@ package com.pixelized.desktop.lwa.network import com.pixelized.desktop.lwa.repository.settings.SettingsRepository import com.pixelized.shared.lwa.model.alteration.AlterationJson +import com.pixelized.shared.lwa.model.campaign.Campaign import com.pixelized.shared.lwa.model.campaign.CampaignJson import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJson import com.pixelized.shared.lwa.protocol.rest.CharacterPreviewJson @@ -25,7 +26,7 @@ class LwaClientImpl( .body() override suspend fun character(id: String): CharacterSheetJson = client - .get("$root/character/detail?id=$id") + .get("$root/character/detail?characterSheetId=$id") .body() override suspend fun updateCharacter(sheet: CharacterSheetJson) = client @@ -35,8 +36,8 @@ class LwaClientImpl( } .body() - override suspend fun deleteCharacter(id: String) = client - .delete("$root/character/delete?id=$id") + override suspend fun deleteCharacterSheet(id: String) = client + .delete("$root/character/delete?characterSheetId=$id") .body() override suspend fun campaign(): CampaignJson = client @@ -49,11 +50,11 @@ class LwaClientImpl( .put("$root/campaign/character/update?characterSheetId=$characterSheetId") .body() - override suspend fun campaignDeleteCharacter( + override suspend fun campaignRemoveCharacter( characterSheetId: String, instanceId: Int, ) = client - .delete("$root/campaign/character/delete?characterSheetId=$characterSheetId&instanceId=$instanceId") + .delete("$root/campaign/character/delete?characterSheetId=$characterSheetId&instanceId=$instanceId&prefix=${Campaign.CharacterInstance.Id.PLAYER}") .body() override suspend fun campaignAddNpc( @@ -62,11 +63,11 @@ class LwaClientImpl( .put("$root/campaign/npc/update?characterSheetId=$characterSheetId") .body() - override suspend fun campaignDeleteNpc( + override suspend fun campaignRemoveNpc( characterSheetId: String, instanceId: Int, ) = client - .delete("$root/campaign/npc/delete?characterSheetId=$characterSheetId&instanceId=$instanceId") + .delete("$root/campaign/npc/delete?characterSheetId=$characterSheetId&instanceId=$instanceId&prefix=${Campaign.CharacterInstance.Id.NPC}") .body() override suspend fun alterations(): List = client @@ -74,18 +75,20 @@ class LwaClientImpl( .body() override suspend fun activeAlterations( + prefix: Char, characterSheetId: String, instanceId: Int, ): List = client - .get("$root/alterations/active?characterSheetId=$characterSheetId&instanceId=$instanceId") + .get("$root/alterations/active?characterSheetId=$characterSheetId&instanceId=$instanceId&prefix=$prefix") .body() override suspend fun toggleActiveAlterations( + prefix: Char, characterSheetId: String, instanceId: Int, alterationId: String, ) = client - .put("$root/alterations/active/toggle?characterSheetId=$characterSheetId&instanceId=$instanceId") { + .put("$root/alterations/active/toggle?characterSheetId=$characterSheetId&instanceId=$instanceId&prefix=$prefix") { contentType(ContentType.Application.Json) setBody(alterationId) } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/alteration/AlterationRepository.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/alteration/AlterationRepository.kt index 26e685e..0d21776 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/alteration/AlterationRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/alteration/AlterationRepository.kt @@ -63,7 +63,7 @@ class AlterationRepository( ) { // alteration was active for the character toggle it off. store.toggleActiveAlteration( - characterInstance = characterInstanceId, + characterInstanceId = characterInstanceId, alterationId = alterationId, ) } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/alteration/AlterationStore.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/alteration/AlterationStore.kt index 8c6804e..01d122e 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/alteration/AlterationStore.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/alteration/AlterationStore.kt @@ -61,6 +61,7 @@ class AlterationStore( characterInstanceId: CharacterInstance.Id, ): List { val request = client.activeAlterations( + prefix = characterInstanceId.prefix, characterSheetId = characterInstanceId.characterSheetId, instanceId = characterInstanceId.instanceId, ) @@ -76,12 +77,13 @@ class AlterationStore( } suspend fun toggleActiveAlteration( - characterInstance: CharacterInstance.Id, + characterInstanceId: CharacterInstance.Id, alterationId: String, ) { client.toggleActiveAlterations( - characterSheetId = characterInstance.characterSheetId, - instanceId = characterInstance.instanceId, + prefix = characterInstanceId.prefix, + characterSheetId = characterInstanceId.characterSheetId, + instanceId = characterInstanceId.instanceId, alterationId = alterationId, ) } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/campaign/CampaignRepository.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/campaign/CampaignRepository.kt index 776c1ea..c303dc4 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/campaign/CampaignRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/campaign/CampaignRepository.kt @@ -21,20 +21,46 @@ class CampaignRepository( store.update() } - fun characterInstanceFlow( - id: Campaign.CharacterInstance.Id, + fun instanceFlow( + characterInstanceId: Campaign.CharacterInstance.Id, ): StateFlow { return campaignFlow .mapNotNull { - it.characters[id] + it.characters[characterInstanceId] ?: it.npcs[characterInstanceId] } .stateIn( scope = scope, started = SharingStarted.Eagerly, - initialValue = campaignFlow.value.character(id = id), + initialValue = instance(characterInstanceId), ) } + fun instance( + characterInstanceId: Campaign.CharacterInstance.Id, + ): Campaign.CharacterInstance { + return campaignFlow.value.let { + it.characters[characterInstanceId] + ?: it.npcs[characterInstanceId] + ?: Campaign.CharacterInstance.empty() + } + } + + @Deprecated(message = "Check if deprecated") + fun characterInstanceFlow( + characterInstanceId: Campaign.CharacterInstance.Id, + ): StateFlow { + return campaignFlow + .mapNotNull { + it.characters[characterInstanceId] + } + .stateIn( + scope = scope, + started = SharingStarted.Eagerly, + initialValue = campaignFlow.value.character(id = characterInstanceId), + ) + } + @Deprecated(message = "Check if deprecated") + fun characterInstance( characterInstanceId: Campaign.CharacterInstance.Id, ): Campaign.CharacterInstance { diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/campaign/CampaignStore.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/campaign/CampaignStore.kt index f2558bc..4ccfe87 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/campaign/CampaignStore.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/campaign/CampaignStore.kt @@ -59,7 +59,7 @@ class CampaignStore( characterSheetId: String, instanceId: Int, ) { - client.campaignDeleteCharacter( + client.campaignRemoveCharacter( characterSheetId = characterSheetId, instanceId = instanceId, ) @@ -77,7 +77,7 @@ class CampaignStore( characterSheetId: String, instanceId: Int, ) { - client.campaignDeleteNpc( + client.campaignRemoveNpc( characterSheetId = characterSheetId, instanceId = instanceId, ) @@ -93,6 +93,7 @@ class CampaignStore( is CampaignMessage -> { val instanceId = Campaign.CharacterInstance.Id( + prefix = payload.prefix, characterSheetId = payload.characterSheetId, instanceId = payload.instanceId, ) diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetStore.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetStore.kt index 784ae18..c103119 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetStore.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetStore.kt @@ -86,7 +86,7 @@ class CharacterSheetStore( characterId: String, ) { try { - client.deleteCharacter(id = characterId) + client.deleteCharacterSheet(id = characterId) _detailFlow.delete(characterId = characterId) } catch (exception: Exception) { // TODO diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/characteristic/CharacterDetailCharacteristicDialogViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/characteristic/CharacterDetailCharacteristicDialogViewModel.kt index 8b2b816..647f697 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/characteristic/CharacterDetailCharacteristicDialogViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/characteristic/CharacterDetailCharacteristicDialogViewModel.kt @@ -41,7 +41,7 @@ class CharacterDetailCharacteristicDialogViewModel( val sheet: CharacterSheet? = characterSheetRepository.characterDetail( characterSheetId = characterInstanceId.characterSheetId, ) - val characterInstance: Campaign.CharacterInstance = campaignRepository.characterInstance( + val characterInstance: Campaign.CharacterInstance = campaignRepository.instance( characterInstanceId = characterInstanceId, ) val alterations: Map> = alterationRepository.alterations( @@ -80,6 +80,7 @@ class CharacterDetailCharacteristicDialogViewModel( // share the data through the websocket. network.share( payload = CampaignMessage.UpdateCharacteristic( + prefix = characterInstanceId.prefix, characterSheetId = characterInstanceId.characterSheetId, instanceId = characterInstanceId.instanceId, characteristic = characteristicJson, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/CharacterSheetDestination.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/CharacterSheetDestination.kt index 0c0d785..092b0ee 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/CharacterSheetDestination.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/CharacterSheetDestination.kt @@ -14,12 +14,17 @@ object CharacterSheetDestination { private const val ROUTE = "character.sheet" private const val CHARACTER_SHEET_ID = "sheetId" private const val CHARACTER_INSTANCE_ID = "instanceId" + private const val CHARACTER_PREFIX = "prefix" - fun baseRoute() = "$ROUTE?${CHARACTER_SHEET_ID.ARG}&${CHARACTER_INSTANCE_ID.ARG}" + fun baseRoute() = ROUTE + + "?${CHARACTER_PREFIX.ARG}" + + "&${CHARACTER_INSTANCE_ID.ARG}" + + "&${CHARACTER_SHEET_ID.ARG}" fun navigationRoute(characterInstanceId: Campaign.CharacterInstance.Id) = ROUTE + - "?$CHARACTER_SHEET_ID=${characterInstanceId.characterSheetId}" + - "&$CHARACTER_INSTANCE_ID=${characterInstanceId.instanceId}" + "?$CHARACTER_PREFIX=${characterInstanceId.prefix}" + + "&$CHARACTER_INSTANCE_ID=${characterInstanceId.instanceId}" + + "&$CHARACTER_SHEET_ID=${characterInstanceId.characterSheetId}" fun arguments() = listOf( navArgument(CHARACTER_SHEET_ID) { @@ -30,6 +35,10 @@ object CharacterSheetDestination { nullable = false type = NavType.IntType }, + navArgument(CHARACTER_PREFIX) { + nullable = false + type = NavType.StringType + } ) data class Argument( @@ -37,6 +46,7 @@ object CharacterSheetDestination { ) { constructor(savedStateHandle: SavedStateHandle) : this( characterInstanceId = Campaign.CharacterInstance.Id( + savedStateHandle.get(CHARACTER_PREFIX)?.getOrNull(0) ?: error("missing character id"), savedStateHandle.get(CHARACTER_SHEET_ID) ?: error("missing character id"), savedStateHandle.get(CHARACTER_INSTANCE_ID) ?: error("missing character id"), ), diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/LevelUpDestination.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/LevelUpDestination.kt index 9d24eec..c67be9e 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/LevelUpDestination.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/LevelUpDestination.kt @@ -12,14 +12,20 @@ import com.pixelized.shared.lwa.model.campaign.Campaign object LevelUpDestination { private const val ROUTE = "levelUp" + private const val CHARACTER_SHEET_ID = "sheetId" private const val CHARACTER_INSTANCE_ID = "instanceId" + private const val CHARACTER_PREFIX = "prefix" - fun baseRoute() = "${ROUTE}?${CHARACTER_SHEET_ID.ARG}&${CHARACTER_INSTANCE_ID.ARG}" + fun baseRoute() = ROUTE + + "?${CHARACTER_PREFIX.ARG}" + + "&${CHARACTER_INSTANCE_ID.ARG}" + + "&${CHARACTER_SHEET_ID.ARG}" fun navigationRoute(characterInstanceId: Campaign.CharacterInstance.Id) = ROUTE + - "?$CHARACTER_SHEET_ID=${characterInstanceId.characterSheetId}" + - "&$CHARACTER_INSTANCE_ID=${characterInstanceId.instanceId}" + "?$CHARACTER_PREFIX=${characterInstanceId.prefix}" + + "&$CHARACTER_INSTANCE_ID=${characterInstanceId.instanceId}" + + "&$CHARACTER_SHEET_ID=${characterInstanceId.characterSheetId}" fun arguments() = listOf( navArgument(CHARACTER_SHEET_ID) { @@ -30,6 +36,10 @@ object LevelUpDestination { nullable = false type = NavType.IntType }, + navArgument(CHARACTER_PREFIX) { + nullable = false + type = NavType.StringType + } ) data class Argument( @@ -37,6 +47,7 @@ object LevelUpDestination { ) { constructor(savedStateHandle: SavedStateHandle) : this( characterInstanceId = Campaign.CharacterInstance.Id( + savedStateHandle.get(CHARACTER_PREFIX)?.getOrNull(0) ?: error("missing character id"), savedStateHandle.get(CHARACTER_SHEET_ID) ?: error("missing character id"), savedStateHandle.get(CHARACTER_INSTANCE_ID) ?: error("missing character id"), ), diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/overlay/roll/RollViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/overlay/roll/RollViewModel.kt index 34102d0..63dd7a4 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/overlay/roll/RollViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/overlay/roll/RollViewModel.kt @@ -41,8 +41,7 @@ class RollViewModel( private val alteredCharacterSheetFactory: AlteredCharacterSheetFactory, ) : ViewModel() { private var alteredCharacterSheet: AlteredCharacterSheet? = null - private var rollAction: String? = null - private var rollSuccessValue: Int? = null + private var rollAction: RollActionUio? = null var lastRollResult: RollResult = RollResult.Dismissed private set @@ -67,7 +66,6 @@ class RollViewModel( suspend fun cleanRoll() { alteredCharacterSheet = null rollAction = null - rollSuccessValue = null lastRollResult = RollResult.Dismissed @@ -106,10 +104,9 @@ class RollViewModel( alterations = alterations, ) - this.rollAction = roll.rollAction - this.rollSuccessValue = roll.rollSuccessValue + this.rollAction = roll - val rollStep = rollSuccessValue?.let { + val rollStep = roll.rollSuccessValue?.let { skillStepUseCase.computeSkillStep(skill = it) } @@ -119,7 +116,7 @@ class RollViewModel( label = roll.label, value = rollStep?.success?.last ) - _rollDifficulty.value = rollSuccessValue?.let { + _rollDifficulty.value = roll.rollSuccessValue?.let { DifficultyUio( open = false, difficulty = Difficulty.NORMAL, @@ -148,7 +145,7 @@ class RollViewModel( delay(500) _cancellable.value = false // compute the skill critical success to critical failure ranges. - val rollStep = rollSuccessValue?.let { + val rollStep = rollAction.rollSuccessValue?.let { skillStepUseCase.computeSkillStep( skill = when (_rollDifficulty.value?.difficulty) { Difficulty.EASY -> it * 2 @@ -163,7 +160,7 @@ class RollViewModel( // compute the roll (typically use the expression inside the rollAction) val roll = skillComputation.computeRoll( sheet = alteredCharacterSheet, - expression = rollAction, + expression = rollAction.rollAction, ) // check where the roll fall into the rollSteps. @@ -196,7 +193,6 @@ class RollViewModel( launch { shareRollResult( - alteredCharacterSheet = alteredCharacterSheet, rollTitle = rollTitle, roll = roll, rollStep = rollStep, @@ -219,7 +215,7 @@ class RollViewModel( open = false, difficulty = difficulty, ) - val rollStep = rollSuccessValue?.let { + val rollStep = rollAction?.rollSuccessValue?.let { skillStepUseCase.computeSkillStep( skill = when (_rollDifficulty.value?.difficulty) { Difficulty.EASY -> it * 2 @@ -263,15 +259,18 @@ class RollViewModel( } private suspend fun shareRollResult( - alteredCharacterSheet: AlteredCharacterSheet, rollTitle: RollTitleUio, roll: Int, rollStep: SkillStepUseCase.SkillStep?, success: String?, ) { + val rollAction = rollAction ?: return + val payload = RollMessage( id = RollMessage.RollId.create(), - characterSheetId = alteredCharacterSheet.id, + prefix = rollAction.characterInstanceId.prefix, + characterSheetId = rollAction.characterInstanceId.characterSheetId, + instanceId = rollAction.characterInstanceId.instanceId, skillLabel = rollTitle.label, rollValue = roll, resultLabel = success, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/CampaignScreen.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/CampaignScreen.kt index 2eff35d..b249101 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/CampaignScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/CampaignScreen.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import com.pixelized.desktop.lwa.ui.composable.blur.BlurContent import com.pixelized.desktop.lwa.ui.composable.blur.rememberBlurContentController @@ -34,16 +35,17 @@ import com.pixelized.desktop.lwa.ui.composable.character.characteristic.Characte import com.pixelized.desktop.lwa.ui.composable.key.KeyHandler import com.pixelized.desktop.lwa.ui.navigation.screen.LocalScreenController import com.pixelized.desktop.lwa.ui.navigation.screen.destination.navigateToLevelScreen -import com.pixelized.desktop.lwa.ui.screen.campaign.chat.CampaignChat -import com.pixelized.desktop.lwa.ui.screen.campaign.chat.CampaignChatViewModel +import com.pixelized.desktop.lwa.ui.screen.campaign.text.CampaignChat +import com.pixelized.desktop.lwa.ui.screen.campaign.text.CampaignChatViewModel +import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.npc.NpcRibbon import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailPanel import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailViewModel import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDiminishedViewModel -import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.PlayerRibbon +import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.rememberTransitionAnimation +import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.player.PlayerRibbon import com.pixelized.desktop.lwa.ui.screen.campaign.toolbar.CampaignToolbar -import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.dialog.DiminishedStatDialog -import com.pixelized.desktop.lwa.ui.screen.campaign.network.NetworkViewModel import com.pixelized.desktop.lwa.ui.screen.campaign.toolbar.CampaignToolbarViewModel +import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.dialog.DiminishedStatDialog import kotlinx.coroutines.launch import org.koin.compose.viewmodel.koinViewModel @@ -53,11 +55,11 @@ val LocalCampaignLayoutScope = compositionLocalOf { @Composable fun CampaignScreen( - characterDetailViewModel: CharacterDetailViewModel = koinViewModel(), + playerDetailViewModel: CharacterDetailViewModel = koinViewModel(key = "player"), + npcDetailViewModel: CharacterDetailViewModel = koinViewModel(key = "npc"), characteristicDialogViewModel: CharacterDetailCharacteristicDialogViewModel = koinViewModel(), dismissedViewModel: CharacterDiminishedViewModel = koinViewModel(), campaignViewModel: CampaignToolbarViewModel = koinViewModel(), - networkViewModel: NetworkViewModel = koinViewModel(), campaignChatViewModel: CampaignChatViewModel = koinViewModel(), ) { val screen = LocalScreenController.current @@ -67,7 +69,8 @@ fun CampaignScreen( KeyHandler { when { it.type == KeyEventType.KeyUp && it.key == Key.Escape -> { - characterDetailViewModel.hideCharacter() + playerDetailViewModel.hideCharacter() + npcDetailViewModel.hideCharacter() true } @@ -82,7 +85,7 @@ fun CampaignScreen( modifier = Modifier.fillMaxSize(), controller = blurController ) { - CampaignScreenLayout( + CampaignLayout( modifier = Modifier.fillMaxSize(), top = { CampaignToolbar( @@ -101,25 +104,50 @@ fun CampaignScreen( chatViewModel = campaignChatViewModel, ) }, - leftOverlay = { + leftPanel = { PlayerRibbon( modifier = Modifier.fillMaxHeight(), onCharacter = { - characterDetailViewModel.showCharacter(id = it) + playerDetailViewModel.showCharacter(id = it) }, onLevelUp = { screen.navigateToLevelScreen(characterInstanceId = it) } ) }, + leftOverlay = { + CharacterDetailPanel( + modifier = Modifier + .padding(all = 8.dp) + .width(width = 128.dp * 4) + .fillMaxHeight(), + transitionSpec = rememberTransitionAnimation(direction = LayoutDirection.Ltr), + blurController = blurController, + detailViewModel = npcDetailViewModel, + characterDiminishedViewModel = dismissedViewModel, + characteristicDialogViewModel = characteristicDialogViewModel, + ) + }, + rightPanel = { + NpcRibbon( + modifier = Modifier.fillMaxHeight(), + onCharacter = { + npcDetailViewModel.showCharacter(id = it) + }, + onLevelUp = { + + } + ) + }, rightOverlay = { CharacterDetailPanel( modifier = Modifier .padding(all = 8.dp) .width(width = 128.dp * 4) .fillMaxHeight(), + transitionSpec = rememberTransitionAnimation(direction = LayoutDirection.Rtl), blurController = blurController, - detailViewModel = characterDetailViewModel, + detailViewModel = playerDetailViewModel, characterDiminishedViewModel = dismissedViewModel, characteristicDialogViewModel = characteristicDialogViewModel, ) @@ -166,26 +194,34 @@ fun CampaignScreen( } @Composable -private fun CampaignScreenLayout( +private fun CampaignLayout( modifier: Modifier = Modifier, top: @Composable () -> Unit, bottom: @Composable () -> Unit, main: @Composable () -> Unit, chat: @Composable () -> Unit, + leftPanel: @Composable () -> Unit, + rightPanel: @Composable () -> Unit, leftOverlay: @Composable () -> Unit, rightOverlay: @Composable () -> Unit, ) { val density = LocalDensity.current + val leftOverlayState = remember { mutableStateOf(DpSize.Unspecified) } + val leftPanelState = remember { mutableStateOf(DpSize.Unspecified) } val rightOverlayState = remember { mutableStateOf(DpSize.Unspecified) } + val rightPanelState = remember { mutableStateOf(DpSize.Unspecified) } val chatOverlayState = remember { mutableStateOf(DpSize.Unspecified) } val scope = remember { CampaignLayoutScope( leftOverlay = leftOverlayState, + leftPanel = leftPanelState, rightOverlay = rightOverlayState, + rightPanel = rightPanelState, chatOverlay = chatOverlayState, ) } + CompositionLocalProvider( LocalCampaignLayoutScope provides scope, ) { @@ -194,7 +230,7 @@ private fun CampaignScreenLayout( ) { top() Box( - modifier = Modifier.weight(1f, fill = true), + modifier = Modifier.weight(weight = 1f, fill = true), ) { Box( modifier = Modifier @@ -210,6 +246,13 @@ private fun CampaignScreenLayout( ) { chat() } + Box( + modifier = Modifier + .align(alignment = Alignment.CenterStart) + .onSizeChanged { leftPanelState.value = it.toDp(density) }, + ) { + leftPanel() + } Box( modifier = Modifier .align(alignment = Alignment.CenterStart) @@ -217,6 +260,13 @@ private fun CampaignScreenLayout( ) { leftOverlay() } + Box( + modifier = Modifier + .align(alignment = Alignment.CenterEnd) + .onSizeChanged { rightPanelState.value = it.toDp(density) }, + ) { + rightPanel() + } Box( modifier = Modifier .align(alignment = Alignment.CenterEnd) @@ -233,7 +283,9 @@ private fun CampaignScreenLayout( @Stable data class CampaignLayoutScope( val leftOverlay: State, + val leftPanel: State, val rightOverlay: State, + val rightPanel: State, val chatOverlay: State, ) diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/PlayerPortrait.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/CharacterPortrait.kt similarity index 58% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/PlayerPortrait.kt rename to composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/CharacterPortrait.kt index d4e0688..8110f7d 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/PlayerPortrait.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/CharacterPortrait.kt @@ -1,4 +1,4 @@ -package com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon +package com.pixelized.desktop.lwa.ui.screen.campaign.player import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility @@ -53,22 +53,33 @@ import kotlin.math.max import kotlin.math.min @Stable -data class PlayerPortraitUio( +object CharacterPortraitDefault { + val size = DpSize(96.dp, 128.dp) +} + +@Stable +data class CharacterPortraitUio( val id: Campaign.CharacterInstance.Id, val portrait: String?, val name: String, - val hp: Int, - val maxHp: Int, - val pp: Int, - val maxPp: Int, val levelUp: Boolean, -) + val enableDetail: Boolean, + val stats: StatsDetail?, +) { + @Stable + data class StatsDetail( + val hp: Int, + val maxHp: Int, + val pp: Int, + val maxPp: Int, + ) +} @Composable -fun PlayerPortrait( +fun CharacterPortrait( modifier: Modifier = Modifier, - size: DpSize, - character: PlayerPortraitUio, + size: DpSize = CharacterPortraitDefault.size, + character: CharacterPortraitUio, onCharacter: (id: Campaign.CharacterInstance.Id) -> Unit, onLevelUp: (id: Campaign.CharacterInstance.Id) -> Unit, ) { @@ -77,9 +88,9 @@ fun PlayerPortrait( Box( modifier = modifier .size(size = size) - .clip(shape = remember { RoundedCornerShape(8.dp) }) + .clip(shape = MaterialTheme.lwa.shapes.portrait) .background(color = colorScheme.elevated.base1dp) - .clickable { onCharacter(character.id) }, + .clickable(character.enableDetail) { onCharacter(character.id) }, ) { AnimatedContent( targetState = character.portrait, @@ -95,11 +106,6 @@ fun PlayerPortrait( ) } - BloodOverlay( - maxHp = character.maxHp.toFloat(), - hp = character.hp.toFloat(), - ) - AnimatedVisibility( modifier = Modifier.offset(x = (-8).dp, y = (-8).dp), visible = character.levelUp, @@ -115,59 +121,67 @@ fun PlayerPortrait( } } - Column( - modifier = Modifier - .fillMaxSize() - .drawWithContent { - drawRect(brush = colorScheme.portraitBackgroundBrush) - drawContent() + character.stats?.let { stats -> + + BloodOverlay( + maxHp = stats.maxHp.toFloat(), + hp = stats.hp.toFloat(), + ) + + Column( + modifier = Modifier + .fillMaxSize() + .drawWithContent { + drawRect(brush = colorScheme.portraitBackgroundBrush) + drawContent() + } + .padding(vertical = 2.dp, horizontal = 4.dp), + verticalArrangement = Arrangement.aligned(alignment = Alignment.Bottom), + ) { + Row { + Icon( + modifier = Modifier.size(12.dp).offset(y = 3.dp), + painter = painterResource(Res.drawable.ic_heart_24dp), + contentDescription = null + ) + Spacer( + modifier = Modifier.width(width = 2.dp), + ) + Text( + modifier = Modifier.alignByBaseline(), + style = MaterialTheme.typography.caption, + fontWeight = FontWeight.Bold, + text = "${stats.hp}", + ) + Text( + modifier = Modifier.alignByBaseline(), + style = MaterialTheme.typography.caption, + fontWeight = FontWeight.Light, + text = "/${stats.maxHp}", + ) + } + Row { + Icon( + modifier = Modifier.size(12.dp).offset(y = 2.dp), + painter = painterResource(Res.drawable.ic_water_drop_24dp), + contentDescription = null + ) + Spacer( + modifier = Modifier.width(width = 2.dp), + ) + Text( + modifier = Modifier.alignByBaseline(), + style = MaterialTheme.typography.caption, + fontWeight = FontWeight.Bold, + text = "${stats.pp}", + ) + Text( + modifier = Modifier.alignByBaseline(), + fontWeight = FontWeight.Light, + style = MaterialTheme.typography.caption, + text = "/${stats.maxPp}", + ) } - .padding(vertical = 2.dp, horizontal = 4.dp), - verticalArrangement = Arrangement.aligned(alignment = Alignment.Bottom), - ) { - Row { - Icon( - modifier = Modifier.size(12.dp).offset(y = 3.dp), - painter = painterResource(Res.drawable.ic_heart_24dp), - contentDescription = null - ) - Spacer( - modifier = Modifier.width(width = 2.dp), - ) - Text( - modifier = Modifier.alignByBaseline(), - style = MaterialTheme.typography.caption, - fontWeight = FontWeight.Bold, - text = "${character.hp}", - ) - Text( - modifier = Modifier.alignByBaseline(), - style = MaterialTheme.typography.caption, - fontWeight = FontWeight.Light, - text = "/${character.maxHp}", - ) - } - Row { - Icon( - modifier = Modifier.size(12.dp).offset(y = 2.dp), - painter = painterResource(Res.drawable.ic_water_drop_24dp), - contentDescription = null - ) - Spacer( - modifier = Modifier.width(width = 2.dp), - ) - Text( - modifier = Modifier.alignByBaseline(), - style = MaterialTheme.typography.caption, - fontWeight = FontWeight.Bold, - text = "${character.pp}", - ) - Text( - modifier = Modifier.alignByBaseline(), - fontWeight = FontWeight.Light, - style = MaterialTheme.typography.caption, - text = "/${character.maxPp}", - ) } } } @@ -184,7 +198,7 @@ private fun BloodOverlay( targetValue = min(maxHp, max(0f, (maxHp - hp) / maxHp)), animationSpec = tween(durationMillis = 350, easing = EaseOutCirc) ) - val animatedColor = animateColorAsState( + val animatedColor = animateColorAsState( targetValue = bloodColor.copy(alpha = ((maxHp - hp) / maxHp) / 4f + .25f) ) Box( diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/PlayerPortraitRoll.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/CharacterPortraitRoll.kt similarity index 90% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/PlayerPortraitRoll.kt rename to composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/CharacterPortraitRoll.kt index 4966f6b..d4b0a66 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/PlayerPortraitRoll.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/CharacterPortraitRoll.kt @@ -1,4 +1,4 @@ -package com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon +package com.pixelized.desktop.lwa.ui.screen.campaign.player import androidx.compose.animation.AnimatedContent import androidx.compose.animation.SizeTransform @@ -37,20 +37,21 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp +import com.pixelized.shared.lwa.model.campaign.Campaign import kotlinx.coroutines.launch import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.ic_d20_24dp import org.jetbrains.compose.resources.painterResource @Stable -data class PlayerPortraitRollUio( - val characterId: String, +data class CharacterPortraitRollUio( + val characterId: Campaign.CharacterInstance.Id, val value: Int?, val label: String?, ) @Stable -data class PlayerPortraitRollAnimation( +data class CharacterPortraitRollAnimation( val alpha: Animatable = Animatable(0f), val rotation: Animatable = Animatable(0f), val scale: Animatable = Animatable(1f), @@ -58,12 +59,12 @@ data class PlayerPortraitRollAnimation( @OptIn(ExperimentalFoundationApi::class) @Composable -fun PlayerPortraitRoll( +fun CharacterPortraitRoll( modifier: Modifier = Modifier, - size: DpSize, - value: PlayerPortraitRollUio?, - onLeftClick: (PlayerPortraitRollUio) -> Unit, - onRightClick: (PlayerPortraitRollUio) -> Unit, + size: DpSize = CharacterPortraitDefault.size, + value: CharacterPortraitRollUio?, + onLeftClick: (CharacterPortraitRollUio) -> Unit, + onRightClick: (CharacterPortraitRollUio) -> Unit, ) { AnimatedContent( modifier = modifier @@ -133,9 +134,9 @@ fun PlayerPortraitRoll( } @Composable -private fun diceIconAnimation(key: Any = Unit): PlayerPortraitRollAnimation { +private fun diceIconAnimation(key: Any = Unit): CharacterPortraitRollAnimation { val animation = remember(key) { - PlayerPortraitRollAnimation() + CharacterPortraitRollAnimation() } LaunchedEffect(key) { launch { diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/PlayerRibbonFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/CharacterRibbonFactory.kt similarity index 62% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/PlayerRibbonFactory.kt rename to composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/CharacterRibbonFactory.kt index d4fb5b4..bff235b 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/PlayerRibbonFactory.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/CharacterRibbonFactory.kt @@ -1,4 +1,4 @@ -package com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon +package com.pixelized.desktop.lwa.ui.screen.campaign.player import com.pixelized.shared.lwa.model.AlteredCharacterSheetFactory import com.pixelized.shared.lwa.model.alteration.FieldAlteration @@ -7,15 +7,17 @@ import com.pixelized.shared.lwa.model.campaign.damage import com.pixelized.shared.lwa.model.campaign.power import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet -class PlayerRibbonFactory( +class CharacterRibbonFactory( private val alteredCharacterSheetFactory: AlteredCharacterSheetFactory, ) { fun convertToPlayerPortraitUio( characterSheet: CharacterSheet?, + alterations: Map>, characterInstanceId: Campaign.CharacterInstance.Id, characterInstance: Campaign.CharacterInstance, - alterations: Map>, - ): PlayerPortraitUio? { + enableDetail: Boolean, + displayCharacterStats: Boolean, + ): CharacterPortraitUio? { if (characterSheet == null) return null val alteredCharacterSheet = alteredCharacterSheetFactory.sheet( @@ -23,15 +25,20 @@ class PlayerRibbonFactory( alterations = alterations, ) - return PlayerPortraitUio( + return CharacterPortraitUio( id = characterInstanceId, portrait = alteredCharacterSheet.thumbnail, name = alteredCharacterSheet.name, - hp = alteredCharacterSheet.maxHp - characterInstance.damage, - maxHp = alteredCharacterSheet.maxHp, - pp = alteredCharacterSheet.maxPp - characterInstance.power, - maxPp = alteredCharacterSheet.maxPp, levelUp = alteredCharacterSheet.shouldLevelUp, + enableDetail = enableDetail, + stats = takeIf { displayCharacterStats }?.let { + CharacterPortraitUio.StatsDetail( + hp = alteredCharacterSheet.maxHp - characterInstance.damage, + maxHp = alteredCharacterSheet.maxHp, + pp = alteredCharacterSheet.maxPp - characterInstance.power, + maxPp = alteredCharacterSheet.maxPp, + ) + }, ) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDetail.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDetail.kt index 8a6b897..e0ec54b 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDetail.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDetail.kt @@ -1,6 +1,8 @@ package com.pixelized.desktop.lwa.ui.screen.campaign.player.detail import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.ContentTransform import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition import androidx.compose.animation.fadeIn @@ -22,9 +24,11 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import com.pixelized.desktop.lwa.LocalRollHostState import com.pixelized.desktop.lwa.ui.composable.blur.BlurContentController @@ -52,6 +56,7 @@ data class CharacterDetailPanelUio( fun CharacterDetailPanel( modifier: Modifier = Modifier, blurController: BlurContentController, + transitionSpec: AnimatedContentTransitionScope.() -> ContentTransform = rememberTransitionAnimation(), detailViewModel: CharacterDetailViewModel, characteristicDialogViewModel: CharacterDetailCharacteristicDialogViewModel, characterDiminishedViewModel: CharacterDiminishedViewModel, @@ -63,6 +68,7 @@ fun CharacterDetailPanel( CharacterDetailAnimatedPanel( modifier = modifier, detail = detail, + transitionSpec = transitionSpec, onDismissRequest = { detailViewModel.hideCharacter() }, @@ -125,6 +131,7 @@ fun CharacterDetailPanel( fun CharacterDetailAnimatedPanel( modifier: Modifier = Modifier, detail: State, + transitionSpec: AnimatedContentTransitionScope.() -> ContentTransform, onDismissRequest: (id: Campaign.CharacterInstance.Id) -> Unit, onDiminished: (id: Campaign.CharacterInstance.Id) -> Unit, onHp: (id: Campaign.CharacterInstance.Id) -> Unit, @@ -140,15 +147,7 @@ fun CharacterDetailAnimatedPanel( AnimatedContent( modifier = Modifier.matchParentSize(), targetState = detail.value, - transitionSpec = { - if (initialState.characterInstanceId != targetState.characterInstanceId) { - val enter = fadeIn() + slideInHorizontally { it / 2 } - val exit = fadeOut() + slideOutHorizontally { it / 2 } - enter togetherWith exit - } else { - EnterTransition.None togetherWith ExitTransition.None - } - } + transitionSpec = transitionSpec, ) { when { it.characterInstanceId == null -> Box( @@ -225,4 +224,23 @@ fun CharacterDetailContent( ) } } +} + +@Composable +@Stable +fun rememberTransitionAnimation( + direction: LayoutDirection = LayoutDirection.Rtl, +) : AnimatedContentTransitionScope.() -> ContentTransform { + return remember { + val mul = if (direction == LayoutDirection.Rtl) 1 else -1 + { + if (initialState.characterInstanceId != targetState.characterInstanceId) { + val enter = fadeIn() + slideInHorizontally { mul * it / 2 } + val exit = fadeOut() + slideOutHorizontally { mul * it / 2 } + enter togetherWith exit + } else { + EnterTransition.None togetherWith ExitTransition.None + } + } + } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDetailViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDetailViewModel.kt index ebff38d..9401c13 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDetailViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDetailViewModel.kt @@ -31,7 +31,7 @@ class CharacterDetailViewModel( CharacterDetailPanelUio( characterInstanceId = characterInstanceId, header = combine( - campaignRepository.characterInstanceFlow(id = characterInstanceId), + campaignRepository.instanceFlow(characterInstanceId = characterInstanceId), characterSheetRepository.characterDetailFlow(characterSheetId = characterInstanceId.characterSheetId), alterationRepository.alterationsFlow(characterInstanceId = characterInstanceId), ) { characterInstance, characterSheet, alterations -> @@ -47,7 +47,7 @@ class CharacterDetailViewModel( initialValue = null, ), sheet = combine( - campaignRepository.characterInstanceFlow(id = characterInstanceId), + campaignRepository.instanceFlow(characterInstanceId = characterInstanceId), characterSheetRepository.characterDetailFlow(characterSheetId = characterInstanceId.characterSheetId), alterationRepository.alterationsFlow(characterInstanceId = characterInstanceId), ) { characterInstance, characterSheet, alterations -> diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDiminishedViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDiminishedViewModel.kt index 8f99b27..dbf7503 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDiminishedViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDiminishedViewModel.kt @@ -57,6 +57,7 @@ class CharacterDiminishedViewModel( val diminished = dialog.value().text.toIntOrNull() ?: 0 networkRepository.share( payload = CampaignMessage.UpdateDiminished( + prefix = dialog.characterInstanceId.prefix, characterSheetId = dialog.characterInstanceId.characterSheetId, instanceId = dialog.characterInstanceId.instanceId, diminished = diminished, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/PlayerRibbonViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/PlayerRibbonViewModel.kt deleted file mode 100644 index 31636d1..0000000 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/PlayerRibbonViewModel.kt +++ /dev/null @@ -1,88 +0,0 @@ -package com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.Stable -import androidx.compose.runtime.State -import androidx.compose.runtime.mutableStateOf -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.pixelized.desktop.lwa.repository.alteration.AlterationRepository -import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository -import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository -import com.pixelized.desktop.lwa.repository.roll_history.RollHistoryRepository -import com.pixelized.desktop.lwa.repository.settings.SettingsRepository -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.flatMapMerge -import kotlinx.coroutines.flow.stateIn -import java.text.Collator - -class PlayerRibbonViewModel( - private val rollHistoryRepository: RollHistoryRepository, - private val settingsRepository: SettingsRepository, - characterRepository: CharacterSheetRepository, - alterationRepository: AlterationRepository, - private val ribbonFactory: PlayerRibbonFactory, - campaignRepository: CampaignRepository, -) : ViewModel() { - - @OptIn(ExperimentalCoroutinesApi::class) - val characters: StateFlow> = campaignRepository.campaignFlow - .flatMapMerge { campaign -> - combine>( - flows = campaign.characters.map { entry -> - combine( - characterRepository.characterDetailFlow(characterSheetId = entry.key.characterSheetId), - alterationRepository.alterationsFlow(characterInstanceId = entry.key), - ) { sheet, alterations -> - ribbonFactory.convertToPlayerPortraitUio( - characterSheet = sheet, - characterInstanceId = entry.key, - characterInstance = entry.value, - alterations = alterations, - ) - } - }, - transform = { headers -> - headers.mapNotNull { it } - .sortedWith(compareBy(Collator.getInstance()) { it.name }) - .toList() - } - ) - } - .stateIn( - scope = viewModelScope, - started = SharingStarted.Eagerly, - initialValue = emptyList() - ) - - private val rolls = hashMapOf>() - - @Composable - @Stable - fun roll(characterSheetId: String): State { - val state = rolls.getOrPut(characterSheetId) { mutableStateOf(null) } - LaunchedEffect(characterSheetId) { - rollHistoryRepository.rolls.collect { roll -> - if (settingsRepository.settings().dynamicDice) { - if (roll.characterSheetId == characterSheetId) { - state.value = PlayerPortraitRollUio( - characterId = characterSheetId, - value = roll.rollValue, - label = roll.resultLabel?.split(" ")?.joinToString(separator = "\n") { it } - ) - } - } - } - } - return state - } - - fun onPortraitRollRightClick(characterId: String) { - rolls[characterId]?.value = null - } -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/npc/NpcRibbon.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/npc/NpcRibbon.kt new file mode 100644 index 0000000..8612b31 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/npc/NpcRibbon.kt @@ -0,0 +1,57 @@ +package com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.npc + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.pixelized.desktop.lwa.ui.screen.campaign.player.CharacterPortrait +import com.pixelized.desktop.lwa.ui.screen.campaign.player.CharacterPortraitDefault +import com.pixelized.desktop.lwa.ui.screen.campaign.player.CharacterPortraitRoll +import com.pixelized.shared.lwa.model.campaign.Campaign +import org.koin.compose.viewmodel.koinViewModel + +@Composable +fun NpcRibbon( + modifier: Modifier = Modifier, + viewModel: NpcRibbonViewModel = koinViewModel(), + padding: PaddingValues = PaddingValues(all = 8.dp), + onCharacter: (id: Campaign.CharacterInstance.Id) -> Unit, + onLevelUp: (id: Campaign.CharacterInstance.Id) -> Unit, +) { + val characters = viewModel.characters.collectAsState() + + LazyColumn( + modifier = modifier, + contentPadding = padding, + verticalArrangement = Arrangement.spacedBy(space = 8.dp) + ) { + items( + items = characters.value, + key = { it.id }, + ) { + Row { + CharacterPortraitRoll( + size = CharacterPortraitDefault.size, + value = viewModel.roll(characterId = it.id).value, + onRightClick = { + viewModel.onPortraitRollRightClick(characterId = it.characterId) + }, + onLeftClick = { + + }, + ) + CharacterPortrait( + size = CharacterPortraitDefault.size, + character = it, + onCharacter = onCharacter, + onLevelUp = onLevelUp, + ) + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/npc/NpcRibbonViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/npc/NpcRibbonViewModel.kt new file mode 100644 index 0000000..96aea29 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/npc/NpcRibbonViewModel.kt @@ -0,0 +1,114 @@ +package com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.npc + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.Stable +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.pixelized.desktop.lwa.repository.alteration.AlterationRepository +import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository +import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository +import com.pixelized.desktop.lwa.repository.roll_history.RollHistoryRepository +import com.pixelized.desktop.lwa.repository.settings.SettingsRepository +import com.pixelized.desktop.lwa.ui.screen.campaign.player.CharacterPortraitRollUio +import com.pixelized.desktop.lwa.ui.screen.campaign.player.CharacterPortraitUio +import com.pixelized.desktop.lwa.ui.screen.campaign.player.CharacterRibbonFactory +import com.pixelized.shared.lwa.model.campaign.Campaign +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapMerge +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.stateIn +import java.text.Collator + +private typealias CharacterId = Campaign.CharacterInstance.Id + +class NpcRibbonViewModel( + private val rollHistoryRepository: RollHistoryRepository, + private val settingsRepository: SettingsRepository, + characterRepository: CharacterSheetRepository, + alterationRepository: AlterationRepository, + campaignRepository: CampaignRepository, + private val ribbonFactory: CharacterRibbonFactory, +) : ViewModel() { + private val rolls = hashMapOf>() + + @OptIn(ExperimentalCoroutinesApi::class) + val characters: StateFlow> = campaignRepository.campaignFlow + .flatMapMerge { campaign -> + if (campaign.npcs.isEmpty()) { + flowOf(emptyList()) + } else { + combine>( + flows = campaign.npcs.map { entry -> + combine( + characterRepository.characterDetailFlow(characterSheetId = entry.key.characterSheetId), + alterationRepository.alterationsFlow(characterInstanceId = entry.key), + ) { sheet, alterations -> + ribbonFactory.convertToPlayerPortraitUio( + characterSheet = sheet, + alterations = alterations, + characterInstanceId = entry.key, + characterInstance = entry.value, + enableDetail = settingsRepository.settings().isGM, + displayCharacterStats = settingsRepository.settings().isGM, + ) + } + }, + transform = { headers -> + headers.mapNotNull { it } + .sortedWith(compareBy(Collator.getInstance()) { it.name }) + .toList() + } + ) + } + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = emptyList() + ) + + @Composable + @Stable + fun roll( + characterId: Campaign.CharacterInstance.Id, + ): State { + val state = rolls.getOrPut(characterId) { mutableStateOf(null) } + + LaunchedEffect(characterId) { + combine( + settingsRepository.settingsFlow(), + rollHistoryRepository.rolls, + ) { settings, roll -> + if (settings.dynamicDice && + characterId.equals(roll.prefix, roll.characterSheetId, roll.instanceId) + ) { + state.value = CharacterPortraitRollUio( + characterId = Campaign.CharacterInstance.Id( + prefix = characterId.prefix, + characterSheetId = characterId.characterSheetId, + instanceId = characterId.instanceId, + ), + value = roll.rollValue, + label = roll.resultLabel?.split(" ")?.joinToString(separator = "\n") { it } + ) + } + }.launchIn(this) + } + + return state + } + + fun onPortraitRollRightClick( + characterId: Campaign.CharacterInstance.Id, + ) { + rolls[characterId]?.value = null + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/PlayerRibbon.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/player/PlayerRibbon.kt similarity index 68% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/PlayerRibbon.kt rename to composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/player/PlayerRibbon.kt index 523c14c..be80778 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/PlayerRibbon.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/player/PlayerRibbon.kt @@ -1,4 +1,4 @@ -package com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon +package com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.player import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues @@ -8,26 +8,21 @@ import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp +import com.pixelized.desktop.lwa.ui.screen.campaign.player.CharacterPortrait +import com.pixelized.desktop.lwa.ui.screen.campaign.player.CharacterPortraitRoll import com.pixelized.shared.lwa.model.campaign.Campaign import org.koin.compose.viewmodel.koinViewModel -object PlayerRibbon { - object Default { - val size = DpSize(96.dp, 128.dp) - } -} - @Composable fun PlayerRibbon( modifier: Modifier = Modifier, - playerRibbonViewModel: PlayerRibbonViewModel = koinViewModel(), + viewModel: PlayerRibbonViewModel = koinViewModel(), padding: PaddingValues = PaddingValues(all = 8.dp), onCharacter: (id: Campaign.CharacterInstance.Id) -> Unit, onLevelUp: (id: Campaign.CharacterInstance.Id) -> Unit, ) { - val characters = playerRibbonViewModel.characters.collectAsState() + val characters = viewModel.characters.collectAsState() LazyColumn( modifier = modifier, @@ -39,17 +34,15 @@ fun PlayerRibbon( key = { it.id }, ) { Row { - PlayerPortrait( - size = PlayerRibbon.Default.size, + CharacterPortrait( character = it, onCharacter = onCharacter, onLevelUp = onLevelUp, ) - PlayerPortraitRoll( - size = PlayerRibbon.Default.size, - value = playerRibbonViewModel.roll(characterSheetId = it.id.characterSheetId).value, + CharacterPortraitRoll( + value = viewModel.roll(characterId = it.id).value, onRightClick = { - playerRibbonViewModel.onPortraitRollRightClick(characterId = it.characterId) + viewModel.onPortraitRollRightClick(characterId = it.characterId) }, onLeftClick = { diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/player/PlayerRibbonViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/player/PlayerRibbonViewModel.kt new file mode 100644 index 0000000..6baf311 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/player/PlayerRibbonViewModel.kt @@ -0,0 +1,115 @@ +package com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.player + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.Stable +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.pixelized.desktop.lwa.repository.alteration.AlterationRepository +import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository +import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository +import com.pixelized.desktop.lwa.repository.roll_history.RollHistoryRepository +import com.pixelized.desktop.lwa.repository.settings.SettingsRepository +import com.pixelized.desktop.lwa.ui.screen.campaign.player.CharacterPortraitRollUio +import com.pixelized.desktop.lwa.ui.screen.campaign.player.CharacterPortraitUio +import com.pixelized.desktop.lwa.ui.screen.campaign.player.CharacterRibbonFactory +import com.pixelized.shared.lwa.model.campaign.Campaign +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapMerge +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.stateIn +import java.text.Collator + +private typealias CharacterId = Campaign.CharacterInstance.Id + +class PlayerRibbonViewModel( + private val rollHistoryRepository: RollHistoryRepository, + private val settingsRepository: SettingsRepository, + characterRepository: CharacterSheetRepository, + alterationRepository: AlterationRepository, + campaignRepository: CampaignRepository, + private val ribbonFactory: CharacterRibbonFactory, +) : ViewModel() { + + private val rolls = hashMapOf>() + + @OptIn(ExperimentalCoroutinesApi::class) + val characters: StateFlow> = campaignRepository.campaignFlow + .flatMapMerge { campaign -> + if (campaign.characters.isEmpty()) { + flowOf(emptyList()) + } else { + combine>( + flows = campaign.characters.map { entry -> + combine( + characterRepository.characterDetailFlow(characterSheetId = entry.key.characterSheetId), + alterationRepository.alterationsFlow(characterInstanceId = entry.key), + ) { sheet, alterations -> + ribbonFactory.convertToPlayerPortraitUio( + characterSheet = sheet, + alterations = alterations, + characterInstanceId = entry.key, + characterInstance = entry.value, + enableDetail = true, + displayCharacterStats = true, + ) + } + }, + transform = { headers -> + headers.mapNotNull { it } + .sortedWith(compareBy(Collator.getInstance()) { it.name }) + .toList() + } + ) + } + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = emptyList() + ) + + @Composable + @Stable + fun roll( + characterId: Campaign.CharacterInstance.Id, + ): State { + val state = rolls.getOrPut(characterId) { mutableStateOf(null) } + + LaunchedEffect(characterId) { + combine( + settingsRepository.settingsFlow(), + rollHistoryRepository.rolls, + ) { settings, roll -> + if (settings.dynamicDice && + characterId.equals(roll.prefix, roll.characterSheetId, roll.instanceId) + ) { + state.value = CharacterPortraitRollUio( + characterId = Campaign.CharacterInstance.Id( + prefix = characterId.prefix, + characterSheetId = characterId.characterSheetId, + instanceId = characterId.instanceId, + ), + value = roll.rollValue, + label = roll.resultLabel?.split(" ")?.joinToString(separator = "\n") { it } + ) + } + }.launchIn(this) + } + + return state + } + + fun onPortraitRollRightClick( + characterId: Campaign.CharacterInstance.Id, + ) { + rolls[characterId]?.value = null + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/chat/CampaignChat.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/text/CampaignChat.kt similarity index 86% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/chat/CampaignChat.kt rename to composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/text/CampaignChat.kt index cfd0e49..2c4b8eb 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/chat/CampaignChat.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/text/CampaignChat.kt @@ -1,4 +1,4 @@ -package com.pixelized.desktop.lwa.ui.screen.campaign.chat +package com.pixelized.desktop.lwa.ui.screen.campaign.text import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.background @@ -36,14 +36,14 @@ import com.pixelized.desktop.lwa.repository.settings.model.Settings import com.pixelized.desktop.lwa.ui.navigation.window.LocalWindowState import com.pixelized.desktop.lwa.ui.screen.campaign.CampaignLayoutScope import com.pixelized.desktop.lwa.ui.screen.campaign.LocalCampaignLayoutScope -import com.pixelized.desktop.lwa.ui.screen.campaign.chat.text.CharacteristicTextMessage -import com.pixelized.desktop.lwa.ui.screen.campaign.chat.text.CharacteristicTextMessageUio -import com.pixelized.desktop.lwa.ui.screen.campaign.chat.text.DiminishedTextMessage -import com.pixelized.desktop.lwa.ui.screen.campaign.chat.text.DiminishedTextMessageUio -import com.pixelized.desktop.lwa.ui.screen.campaign.chat.text.RollTextMessage -import com.pixelized.desktop.lwa.ui.screen.campaign.chat.text.RollTextMessageUio -import com.pixelized.desktop.lwa.ui.screen.campaign.chat.text.TextMessage -import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.PlayerRibbon +import com.pixelized.desktop.lwa.ui.screen.campaign.player.CharacterPortraitDefault +import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.CharacteristicTextMessage +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.RollTextMessage +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.theme.lwa import kotlinx.coroutines.launch import org.koin.compose.viewmodel.koinViewModel @@ -73,7 +73,7 @@ fun CampaignChat( modifier = modifier .size( width = animatedChatWidth.value, - height = PlayerRibbon.Default.size.height * 2 + 8.dp, + height = CharacterPortraitDefault.size.height * 2 + 8.dp, ) .graphicsLayer { alpha = chatViewModel.chatAnimatedVisibility.value @@ -154,7 +154,7 @@ private fun rememberAnimatedChatWidth( val maxChatWidth = 64.dp * 12 val windowWidth = windowsState.size.width if (windowWidth != Dp.Unspecified) { - val width = windowWidth - campaignScreenScope.leftOverlay.value.width - 16.dp + val width = windowWidth - campaignScreenScope.leftPanel.value.width - 16.dp min(max(width, minChatWidth), maxChatWidth) } else { minChatWidth diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/chat/CampaignChatViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/text/CampaignChatViewModel.kt similarity index 92% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/chat/CampaignChatViewModel.kt rename to composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/text/CampaignChatViewModel.kt index 43267e2..1a9978b 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/chat/CampaignChatViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/text/CampaignChatViewModel.kt @@ -1,4 +1,4 @@ -package com.pixelized.desktop.lwa.ui.screen.campaign.chat +package com.pixelized.desktop.lwa.ui.screen.campaign.text import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.tween @@ -6,7 +6,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.pixelized.desktop.lwa.repository.network.NetworkRepository import com.pixelized.desktop.lwa.repository.settings.SettingsRepository -import com.pixelized.desktop.lwa.ui.screen.campaign.chat.text.TextMessage +import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.TextMessage import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.mapNotNull diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/chat/TextMessageFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/text/TextMessageFactory.kt similarity index 92% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/chat/TextMessageFactory.kt rename to composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/text/TextMessageFactory.kt index 5f499ba..ba4c107 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/chat/TextMessageFactory.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/text/TextMessageFactory.kt @@ -1,11 +1,11 @@ -package com.pixelized.desktop.lwa.ui.screen.campaign.chat +package com.pixelized.desktop.lwa.ui.screen.campaign.text import com.pixelized.desktop.lwa.repository.alteration.AlterationRepository import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository -import com.pixelized.desktop.lwa.ui.screen.campaign.chat.text.CharacteristicTextMessageUio -import com.pixelized.desktop.lwa.ui.screen.campaign.chat.text.DiminishedTextMessageUio -import com.pixelized.desktop.lwa.ui.screen.campaign.chat.text.RollTextMessageUio -import com.pixelized.desktop.lwa.ui.screen.campaign.chat.text.TextMessage +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.RollTextMessageUio +import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.TextMessage import com.pixelized.shared.lwa.model.AlteredCharacterSheetFactory import com.pixelized.shared.lwa.model.campaign.Campaign import com.pixelized.shared.lwa.model.campaign.CampaignJsonV1.CharacterInstanceJsonV1.CharacteristicV1.Damage @@ -79,6 +79,7 @@ class TextMessageFactory( characterSheetId = payload.characterSheetId, ) ?: return null val characterInstanceId = Campaign.CharacterInstance.Id( + prefix = payload.prefix, characterSheetId = payload.characterSheetId, instanceId = payload.instanceId, ) diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/chat/text/CharacteristicTextMessage.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/text/messages/CharacteristicTextMessage.kt similarity index 96% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/chat/text/CharacteristicTextMessage.kt rename to composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/text/messages/CharacteristicTextMessage.kt index 9328aee..2c28c30 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/chat/text/CharacteristicTextMessage.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/text/messages/CharacteristicTextMessage.kt @@ -1,4 +1,4 @@ -package com.pixelized.desktop.lwa.ui.screen.campaign.chat.text +package com.pixelized.desktop.lwa.ui.screen.campaign.text.messages import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/chat/text/DiminishedTextMessage.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/text/messages/DiminishedTextMessage.kt similarity index 96% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/chat/text/DiminishedTextMessage.kt rename to composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/text/messages/DiminishedTextMessage.kt index 9da5f99..a7491e1 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/chat/text/DiminishedTextMessage.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/text/messages/DiminishedTextMessage.kt @@ -1,4 +1,4 @@ -package com.pixelized.desktop.lwa.ui.screen.campaign.chat.text +package com.pixelized.desktop.lwa.ui.screen.campaign.text.messages import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/chat/text/RollTextMessage.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/text/messages/RollTextMessage.kt similarity index 98% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/chat/text/RollTextMessage.kt rename to composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/text/messages/RollTextMessage.kt index e3c9954..3d477cd 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/chat/text/RollTextMessage.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/text/messages/RollTextMessage.kt @@ -1,4 +1,4 @@ -package com.pixelized.desktop.lwa.ui.screen.campaign.chat.text +package com.pixelized.desktop.lwa.ui.screen.campaign.text.messages import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/chat/text/TextMessage.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/text/messages/TextMessage.kt similarity index 54% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/chat/text/TextMessage.kt rename to composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/text/messages/TextMessage.kt index 9ede501..4965e03 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/chat/text/TextMessage.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/text/messages/TextMessage.kt @@ -1,4 +1,4 @@ -package com.pixelized.desktop.lwa.ui.screen.campaign.chat.text +package com.pixelized.desktop.lwa.ui.screen.campaign.text.messages sealed interface TextMessage { val id : String diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/characterSheet/detail/CharacterSheetViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/characterSheet/detail/CharacterSheetViewModel.kt index 0cf3826..cac6ff5 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/characterSheet/detail/CharacterSheetViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/characterSheet/detail/CharacterSheetViewModel.kt @@ -51,7 +51,7 @@ class CharacterSheetViewModel( val diminishedDialog: State get() = _diminishedDialog val diminishedValueFlow: StateFlow = campaignRepository - .characterInstanceFlow(id = argument.characterInstanceId) + .characterInstanceFlow(characterInstanceId = argument.characterInstanceId) .map { instance -> instance.diminished.takeIf { it > 0 } } .stateIn(scope = viewModelScope, SharingStarted.Lazily, null) @@ -144,6 +144,7 @@ class CharacterSheetViewModel( val diminished = dialog.value().text.toIntOrNull() ?: 0 network.share( payload = CampaignMessage.UpdateDiminished( + prefix = dialog.characterInstanceId.prefix, characterSheetId = dialog.characterInstanceId.characterSheetId, instanceId = dialog.characterInstanceId.instanceId, diminished = diminished, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/levelup/LevelUpViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/levelup/LevelUpViewModel.kt index 16ca8ab..8cc25ed 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/levelup/LevelUpViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/levelup/LevelUpViewModel.kt @@ -6,7 +6,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository import com.pixelized.desktop.lwa.ui.composable.error.ErrorSnackUio -import com.pixelized.desktop.lwa.ui.navigation.screen.destination.CharacterSheetDestination +import com.pixelized.desktop.lwa.ui.navigation.screen.destination.LevelUpDestination import com.pixelized.desktop.lwa.ui.overlay.roll.RollResult import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId import kotlinx.coroutines.flow.MutableSharedFlow @@ -26,7 +26,7 @@ class LevelUpViewModel( private val levelUpFactory: LevelUpFactory, savedStateHandle: SavedStateHandle, ) : ViewModel() { - private val argument = CharacterSheetDestination.Argument(savedStateHandle) + private val argument = LevelUpDestination.Argument(savedStateHandle) private val _errors = MutableSharedFlow() val error: SharedFlow = _errors diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/shapes/LwaShapes.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/shapes/LwaShapes.kt index c53ed56..a6d9668 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/shapes/LwaShapes.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/shapes/LwaShapes.kt @@ -9,6 +9,7 @@ import androidx.compose.ui.unit.dp @Stable data class LwaShapes( + val portrait: Shape, val panel: Shape, val settings: Shape, ) @@ -16,10 +17,12 @@ data class LwaShapes( @Stable @Composable fun lwaShapes( - panel: Shape = RoundedCornerShape(16.dp), + portrait: Shape = RoundedCornerShape(8.dp), + panel: Shape = RoundedCornerShape(8.dp), settings: Shape = RoundedCornerShape(8.dp), ): LwaShapes = remember { LwaShapes( + portrait = portrait, panel = panel, settings = settings, ) diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/character/CharacterSheetService.kt b/server/src/main/kotlin/com/pixelized/server/lwa/model/character/CharacterSheetService.kt index 5e1ae08..55fbba8 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/character/CharacterSheetService.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/model/character/CharacterSheetService.kt @@ -30,15 +30,15 @@ class CharacterSheetService( return sheets.map { factory.convertToPreviewJson(sheet = it.value) } } - fun character(id: String): CharacterSheetJson? { + fun characterSheet(id: String): CharacterSheetJson? { return sheets[id]?.let(factory::convertToJson) } - suspend fun updateCharacter(character: CharacterSheetJson) { + suspend fun updateCharacterSheet(character: CharacterSheetJson) { return store.save(sheet = factory.convertFromJson(character)) } - fun deleteCharacter(characterId: String): Boolean { + fun deleteCharacterSheet(characterId: String): Boolean { return store.delete(id = characterId) } diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/Engine.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/Engine.kt index a9efa08..686c650 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/server/Engine.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/Engine.kt @@ -27,6 +27,7 @@ class Engine( is CampaignMessage -> { val instanceId = Campaign.CharacterInstance.Id( + prefix = data.prefix, characterSheetId = data.characterSheetId, instanceId = data.instanceId, ) diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/Server.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/Server.kt index 149dff5..4a05be3 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/server/Server.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/Server.kt @@ -4,7 +4,7 @@ package com.pixelized.server.lwa.server import com.pixelized.server.lwa.server.rest.alteration.getActiveAlteration import com.pixelized.server.lwa.server.rest.alteration.getAlteration import com.pixelized.server.lwa.server.rest.alteration.putActiveAlteration -import com.pixelized.server.lwa.server.rest.campaign.deleteCampaignCharacter +import com.pixelized.server.lwa.server.rest.campaign.removeCampaignCharacter import com.pixelized.server.lwa.server.rest.campaign.deleteCampaignNpc import com.pixelized.server.lwa.server.rest.campaign.getCampaign import com.pixelized.server.lwa.server.rest.campaign.putCampaignCharacter @@ -142,7 +142,7 @@ class LocalServer { ) delete( path = "/delete", - body = engine.deleteCampaignCharacter(), + body = engine.removeCampaignCharacter(), ) } route(path = "/npc") { diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/alteration/GET_Active_Alteration.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/alteration/GET_Active_Alteration.kt index e8203db..6d1f869 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/alteration/GET_Active_Alteration.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/alteration/GET_Active_Alteration.kt @@ -1,26 +1,25 @@ package com.pixelized.server.lwa.server.rest.alteration import com.pixelized.server.lwa.server.Engine -import com.pixelized.shared.lwa.model.campaign.Campaign +import com.pixelized.server.lwa.utils.extentions.characterInstanceId +import io.ktor.http.HttpStatusCode import io.ktor.server.response.respond +import io.ktor.server.response.respondText fun Engine.getActiveAlteration(): suspend io.ktor.server.routing.RoutingContext.() -> Unit { return { - // get the query parameter - val characterSheetId = call.queryParameters["characterSheetId"] - val instanceId = call.queryParameters["instanceId"]?.toIntOrNull() - // build the character instance id. - val id = if (characterSheetId != null && instanceId != null) { - Campaign.CharacterInstance.Id( - characterSheetId = characterSheetId, - instanceId = instanceId + try { + // get the query parameter + val characterInstanceId = call.queryParameters.characterInstanceId + // fetch the data from the service + val data = alterationService.active(characterInstanceId = characterInstanceId) + // respond to the client. + call.respond(data) + } catch (exception: Exception) { + call.respondText( + text = exception.localizedMessage, + status = HttpStatusCode.UnprocessableEntity, ) - } else { - null } - // fetch the data from the service - val data = id?.let { alterationService.active(it) } ?: emptyList() - // respond to the client. - call.respond(data) } } \ No newline at end of file diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/alteration/PUT_ActiveAlteration.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/alteration/PUT_ActiveAlteration.kt index b247518..751a956 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/alteration/PUT_ActiveAlteration.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/alteration/PUT_ActiveAlteration.kt @@ -1,7 +1,7 @@ package com.pixelized.server.lwa.server.rest.alteration import com.pixelized.server.lwa.server.Engine -import com.pixelized.shared.lwa.model.campaign.Campaign +import com.pixelized.server.lwa.utils.extentions.characterInstanceId import com.pixelized.shared.lwa.protocol.websocket.Message import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation import io.ktor.http.HttpStatusCode @@ -10,54 +10,45 @@ import io.ktor.server.response.respondText fun Engine.putActiveAlteration(): suspend io.ktor.server.routing.RoutingContext.() -> Unit { return { - // fetch the query parameters - val characterSheetId = call.queryParameters["characterSheetId"] - val instanceId = call.queryParameters["instanceId"]?.toIntOrNull() - val alterationId = call.receive() + try { + // get the query parameter + val characterInstanceId = call.queryParameters.characterInstanceId - // build the characterInstanceId from the parameters - val characterInstanceId = if (characterSheetId != null && instanceId != null) { - Campaign.CharacterInstance.Id( - characterSheetId = characterSheetId, - instanceId = instanceId - ) - } else { - null - } + // fetch the query parameters + val alterationId = call.receive() - // Update the alteration - val updated = characterInstanceId?.let { - alterationService.toggleActiveAlteration( - characterInstanceId = it, + // Update the alteration + val updated = alterationService.toggleActiveAlteration( + characterInstanceId = characterInstanceId, alterationId = alterationId, ) - } ?: false - - // build the Http response & send it - val code = when (updated) { - true -> HttpStatusCode.Accepted - else -> HttpStatusCode.UnprocessableEntity - } - call.respondText( - text = "$code", - status = code, - ) - - // share the modification to all client through the websocket. - characterInstanceId?.let { + if (!updated) { + error("Unexpected error occurred when toggling the alteration (id:$alterationId) for the character (id:$characterInstanceId)") + } + // build the Http response & send it + call.respondText( + text = "$HttpStatusCode.Accepted", + status = HttpStatusCode.Accepted, + ) + // share the modification to all client through the websocket. webSocket.emit( Message( from = "Server", value = RestSynchronisation.ToggleActiveAlteration( - characterId = campaignJsonFactory.convertToJson(id = it), + characterId = campaignJsonFactory.convertToJson(id = characterInstanceId), alterationId = alterationId, active = alterationService.isAlterationActive( - characterInstanceId = it, + characterInstanceId = characterInstanceId, alterationId = alterationId ), ), ) ) + } catch (exception: Exception) { + call.respondText( + text = exception.localizedMessage, + status = HttpStatusCode.UnprocessableEntity, + ) } } } \ No newline at end of file diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/DELETE_Campaign_character.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/DELETE_Campaign_character.kt index b2cdc7b..aa7afc1 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/DELETE_Campaign_character.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/DELETE_Campaign_character.kt @@ -1,38 +1,39 @@ package com.pixelized.server.lwa.server.rest.campaign import com.pixelized.server.lwa.server.Engine -import com.pixelized.shared.lwa.model.campaign.Campaign +import com.pixelized.server.lwa.utils.extentions.characterInstanceId 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.server.response.respondText -fun Engine.deleteCampaignCharacter(): suspend io.ktor.server.routing.RoutingContext.() -> Unit { +fun Engine.removeCampaignCharacter(): suspend io.ktor.server.routing.RoutingContext.() -> Unit { return { - val characterSheetId = call.queryParameters["characterSheetId"] - val instanceId = call.queryParameters["instanceId"]?.toIntOrNull() - val id = if (characterSheetId != null && instanceId != null) { - Campaign.CharacterInstance.Id( - characterSheetId = characterSheetId, - instanceId = instanceId + try { + // get the query parameter + val characterInstanceId = call.queryParameters.characterInstanceId + // remove the character form the party + val updated = campaignService.removeCharacter(characterInstanceId = characterInstanceId) + // error case + if (!updated) { + error("Unexpected error when removing character (id:$characterInstanceId) from party.") + } + // API & WebSocket responses + call.respondText( + text = "$HttpStatusCode.Accepted", + status = HttpStatusCode.Accepted, ) - } else { - null - } - val updated = id?.let { campaignService.removeCharacter(it) } ?: false - val code = when (updated) { - true -> HttpStatusCode.Accepted - else -> HttpStatusCode.UnprocessableEntity - } - call.respondText( - text = "$code", - status = code, - ) - webSocket.emit( - Message( - from = "Server", - value = RestSynchronisation.Campaign, + webSocket.emit( + Message( + from = "Server", + value = RestSynchronisation.Campaign, + ) ) - ) + } catch (exception: Exception) { + call.respondText( + text = exception.localizedMessage, + status = HttpStatusCode.UnprocessableEntity, + ) + } } } \ No newline at end of file diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/DELETE_Campaign_npc.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/DELETE_Campaign_npc.kt index 82edfb5..0dd016b 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/DELETE_Campaign_npc.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/DELETE_Campaign_npc.kt @@ -1,7 +1,7 @@ package com.pixelized.server.lwa.server.rest.campaign import com.pixelized.server.lwa.server.Engine -import com.pixelized.shared.lwa.model.campaign.Campaign +import com.pixelized.server.lwa.utils.extentions.characterInstanceId import com.pixelized.shared.lwa.protocol.websocket.Message import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation import io.ktor.http.HttpStatusCode @@ -9,30 +9,31 @@ import io.ktor.server.response.respondText fun Engine.deleteCampaignNpc(): suspend io.ktor.server.routing.RoutingContext.() -> Unit { return { - val characterSheetId = call.queryParameters["characterSheetId"] - val instanceId = call.queryParameters["instanceId"]?.toIntOrNull() - val id = if (characterSheetId != null && instanceId != null) { - Campaign.CharacterInstance.Id( - characterSheetId = characterSheetId, - instanceId = instanceId + try { + // get the query parameter + val characterInstanceId = call.queryParameters.characterInstanceId + // remove the character form the party + val updated = campaignService.removeNpc(npcInstanceId = characterInstanceId) + // error case + if (!updated) { + error("Unexpected error when removing character (id:$characterInstanceId) from npcs.") + } + // API & WebSocket responses + call.respondText( + text = "$HttpStatusCode.Accepted", + status = HttpStatusCode.Accepted, ) - } else { - null - } - val updated = id?.let { campaignService.removeNpc(it) } ?: false - val code = when (updated) { - true -> HttpStatusCode.Accepted - else -> HttpStatusCode.UnprocessableEntity - } - call.respondText( - text = "$code", - status = code, - ) - webSocket.emit( - Message( - from = "Server", - value = RestSynchronisation.Campaign, + webSocket.emit( + Message( + from = "Server", + value = RestSynchronisation.Campaign, + ) ) - ) + } catch (exception: Exception) { + call.respondText( + text = exception.localizedMessage, + status = HttpStatusCode.UnprocessableEntity, + ) + } } } \ No newline at end of file diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/PUT_Campaign_character.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/PUT_Campaign_character.kt index c5368bb..e22a35e 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/PUT_Campaign_character.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/PUT_Campaign_character.kt @@ -1,6 +1,7 @@ package com.pixelized.server.lwa.server.rest.campaign import com.pixelized.server.lwa.server.Engine +import com.pixelized.server.lwa.utils.extentions.characterSheetId import com.pixelized.shared.lwa.model.campaign.Campaign import com.pixelized.shared.lwa.protocol.websocket.Message import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation @@ -10,25 +11,26 @@ import io.ktor.server.response.respondText fun Engine.putCampaignCharacter(): suspend io.ktor.server.routing.RoutingContext.() -> Unit { return { try { - val characterSheetId = call.queryParameters["characterSheetId"] - ?: error("missing character sheet id") - + // get the query parameter + val characterSheetId = call.queryParameters.characterSheetId + // check if the character is already in the party. val instanceId = campaignService.campaign().characters.keys .firstOrNull { key -> key.characterSheetId == characterSheetId } - + // handle the error case. if (instanceId != null) { - error("Character Already in party") + error("Character (characterSheetId:$characterSheetId) Already in party") } - + // create the instance id for the character. val id = Campaign.CharacterInstance.Id( + prefix = Campaign.CharacterInstance.Id.PLAYER, characterSheetId = characterSheetId, instanceId = 0, ) - + // add the character to the party. if (campaignService.addCharacter(id).not()) { error("Unexpected error occurred when the character instance was added to the party") } - + // API & WebSocket responses. call.respondText( text = "Character $characterSheetId successfully added to the party", status = HttpStatusCode.Accepted, diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/PUT_Campaign_npc.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/PUT_Campaign_npc.kt index dcc1248..ec7b174 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/PUT_Campaign_npc.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/PUT_Campaign_npc.kt @@ -1,6 +1,7 @@ package com.pixelized.server.lwa.server.rest.campaign import com.pixelized.server.lwa.server.Engine +import com.pixelized.server.lwa.utils.extentions.characterSheetId import com.pixelized.shared.lwa.model.campaign.Campaign import com.pixelized.shared.lwa.protocol.websocket.Message import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation @@ -10,9 +11,9 @@ import io.ktor.server.response.respondText fun Engine.putCampaignNpc(): suspend io.ktor.server.routing.RoutingContext.() -> Unit { return { try { - val characterSheetId = call.queryParameters["characterSheetId"] - ?: error("missing character sheet id") - + // get the query parameter + val characterSheetId = call.queryParameters.characterSheetId + // compute the npc id base on similar character sheets. val instanceId = campaignService.campaign().npcs.keys .filter { it.characterSheetId == characterSheetId } .reduceOrNull { acc, id -> @@ -22,16 +23,17 @@ fun Engine.putCampaignNpc(): suspend io.ktor.server.routing.RoutingContext.() -> acc } } - + // create the instance id for the character. val id = Campaign.CharacterInstance.Id( + prefix = Campaign.CharacterInstance.Id.NPC, characterSheetId = characterSheetId, instanceId = instanceId?.let { it.instanceId + 1 } ?: 0, ) - + // add the character to the npcs. if (campaignService.addNpc(id).not()) { error("Unexpected error occurred when the character instance was added to the npcs") } - + // API & WebSocket responses. call.respondText( text = "Character $characterSheetId successfully added to the npcs", status = HttpStatusCode.Accepted, diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/character/DELETE_Character.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/character/DELETE_Character.kt index d60bd49..4997dd0 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/character/DELETE_Character.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/character/DELETE_Character.kt @@ -1,6 +1,7 @@ package com.pixelized.server.lwa.server.rest.character import com.pixelized.server.lwa.server.Engine +import com.pixelized.server.lwa.utils.extentions.characterSheetId import com.pixelized.shared.lwa.protocol.websocket.Message import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation import io.ktor.http.HttpStatusCode @@ -8,10 +9,10 @@ import io.ktor.server.response.respondText fun Engine.deleteCharacter(): suspend io.ktor.server.routing.RoutingContext.() -> Unit { return { - val id = call.parameters["id"] - val deleted = id?.let(characterService::deleteCharacter) ?: false + val characterSheetId = call.parameters.characterSheetId + val deleted = characterService.deleteCharacterSheet(characterId = characterSheetId) - if (deleted && id != null) { + if (deleted) { call.respondText( text = "${HttpStatusCode.OK}", status = HttpStatusCode.OK, @@ -19,7 +20,7 @@ fun Engine.deleteCharacter(): suspend io.ktor.server.routing.RoutingContext.() - webSocket.emit( Message( from = "Server", - value = RestSynchronisation.CharacterDelete(characterId = id), + value = RestSynchronisation.CharacterDelete(characterId = characterSheetId), ) ) } else { diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/character/GET_Character.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/character/GET_Character.kt index 8ab2351..f6124ec 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/character/GET_Character.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/character/GET_Character.kt @@ -1,15 +1,16 @@ package com.pixelized.server.lwa.server.rest.character import com.pixelized.server.lwa.server.Engine -import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJson +import com.pixelized.server.lwa.utils.extentions.characterSheetId import io.ktor.http.HttpStatusCode import io.ktor.server.response.respond import io.ktor.server.response.respondText fun Engine.getCharacter(): suspend io.ktor.server.routing.RoutingContext.() -> Unit { return { - val id = call.queryParameters["id"] - val body: CharacterSheetJson? = id?.let(characterService::character) + val id = call.queryParameters.characterSheetId + val body = characterService.characterSheet(id) + if (body != null) { call.respond(body) } else { diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/character/PUT_Character.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/character/PUT_Character.kt index c30f801..4686b65 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/character/PUT_Character.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/character/PUT_Character.kt @@ -11,7 +11,7 @@ import io.ktor.server.response.respondText fun Engine.putCharacter(): suspend io.ktor.server.routing.RoutingContext.() -> Unit { return { val form = call.receive() - characterService.updateCharacter( + characterService.updateCharacterSheet( character = form ) call.respondText( diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/utils/extentions/ParametersExt.kt b/server/src/main/kotlin/com/pixelized/server/lwa/utils/extentions/ParametersExt.kt new file mode 100644 index 0000000..fd90995 --- /dev/null +++ b/server/src/main/kotlin/com/pixelized/server/lwa/utils/extentions/ParametersExt.kt @@ -0,0 +1,20 @@ +package com.pixelized.server.lwa.utils.extentions + +import com.pixelized.shared.lwa.model.campaign.Campaign +import io.ktor.http.Parameters + +val Parameters.characterInstanceId: Campaign.CharacterInstance.Id + get() = Campaign.CharacterInstance.Id( + characterSheetId = characterSheetId, + instanceId = instanceId, + prefix = prefix, + ) + +val Parameters.characterSheetId + get() = this["characterSheetId"] ?: error("Missing character sheet id.") + +val Parameters.instanceId: Int + get() = this["instanceId"]?.toIntOrNull() ?: error("Missing character instance id.") + +val Parameters.prefix: Char + get() = this["prefix"]?.get(0) ?: error("Missing character prefix.") diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/Campaign.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/Campaign.kt index cfcc8ab..5ab1f30 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/Campaign.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/Campaign.kt @@ -10,9 +10,25 @@ data class Campaign( val diminished: Int, ) { data class Id( + val prefix: Char, val characterSheetId: String, val instanceId: Int, - ) + ) { + fun equals( + prefix: Char, + characterSheetId: String?, + instanceId: Int?, + ): Boolean { + return this.prefix == prefix && + this.characterSheetId == characterSheetId && + this.instanceId == instanceId + } + + companion object { + const val PLAYER = 'c' + const val NPC = 'n' + } + } enum class Characteristic { Damage, diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/factory/CampaignJsonFactory.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/factory/CampaignJsonFactory.kt index a50027d..017aa3f 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/factory/CampaignJsonFactory.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/factory/CampaignJsonFactory.kt @@ -65,7 +65,7 @@ class CampaignJsonFactory( fun convertToJson( id: Campaign.CharacterInstance.Id, ): String { - return "${String.format("%03d", id.instanceId)}-${id.characterSheetId}" + return "${id.prefix}-${String.format("%03d", id.instanceId)}-${id.characterSheetId}" } fun convertToJson( diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/factory/CampaignJsonV1Factory.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/factory/CampaignJsonV1Factory.kt index 8a7797a..0695336 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/factory/CampaignJsonV1Factory.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/factory/CampaignJsonV1Factory.kt @@ -33,8 +33,9 @@ class CampaignJsonV1Factory { characterInstanceIdJson: String, ): Campaign.CharacterInstance.Id { return Campaign.CharacterInstance.Id( - characterSheetId = characterInstanceIdJson.drop(4), // drop first 3 number then the - - instanceId = characterInstanceIdJson.take(3).toIntOrNull() ?: 0, + prefix = characterInstanceIdJson.take(1)[0], + characterSheetId = characterInstanceIdJson.drop(2 + 4), // drop the char then the - then the first 3 number then the - + instanceId = characterInstanceIdJson.drop(2).take(3).toIntOrNull() ?: 0, ) } diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/payload/CampaignMessage.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/payload/CampaignMessage.kt index 580ecaa..00a6525 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/payload/CampaignMessage.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/payload/CampaignMessage.kt @@ -5,11 +5,13 @@ import kotlinx.serialization.Serializable @Serializable sealed interface CampaignMessage : MessagePayload { + val prefix: Char val characterSheetId: String val instanceId: Int @Serializable data class UpdateCharacteristic( + override val prefix: Char, override val characterSheetId: String, override val instanceId: Int, val characteristic: CampaignJsonV1.CharacterInstanceJsonV1.CharacteristicV1, @@ -18,6 +20,7 @@ sealed interface CampaignMessage : MessagePayload { @Serializable data class UpdateDiminished( + override val prefix: Char, override val characterSheetId: String, override val instanceId: Int, val diminished: Int, diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/payload/RollMessage.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/payload/RollMessage.kt index 2b3716d..d5e76ad 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/payload/RollMessage.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/payload/RollMessage.kt @@ -6,7 +6,9 @@ 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,