Add GM & NPC (UI) support. Change the Id system.
This commit is contained in:
		
							parent
							
								
									6b86a6c075
								
							
						
					
					
						commit
						27dba5438e
					
				
					 54 changed files with 816 additions and 426 deletions
				
			
		| 
						 | 
				
			
			@ -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,
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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<AlterationJson>
 | 
			
		||||
 | 
			
		||||
    suspend fun activeAlterations(characterSheetId: String, instanceId: Int): List<String>
 | 
			
		||||
    suspend fun activeAlterations(
 | 
			
		||||
        prefix: Char,
 | 
			
		||||
        characterSheetId: String,
 | 
			
		||||
        instanceId: Int,
 | 
			
		||||
    ): List<String>
 | 
			
		||||
 | 
			
		||||
    suspend fun toggleActiveAlterations(
 | 
			
		||||
        prefix: Char,
 | 
			
		||||
        characterSheetId: String,
 | 
			
		||||
        instanceId: Int,
 | 
			
		||||
        alterationId: String,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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<Unit>()
 | 
			
		||||
 | 
			
		||||
    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<Unit>()
 | 
			
		||||
 | 
			
		||||
    override suspend fun campaign(): CampaignJson = client
 | 
			
		||||
| 
						 | 
				
			
			@ -49,11 +50,11 @@ class LwaClientImpl(
 | 
			
		|||
        .put("$root/campaign/character/update?characterSheetId=$characterSheetId")
 | 
			
		||||
        .body<Unit>()
 | 
			
		||||
 | 
			
		||||
    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<Unit>()
 | 
			
		||||
 | 
			
		||||
    override suspend fun campaignAddNpc(
 | 
			
		||||
| 
						 | 
				
			
			@ -62,11 +63,11 @@ class LwaClientImpl(
 | 
			
		|||
        .put("$root/campaign/npc/update?characterSheetId=$characterSheetId")
 | 
			
		||||
        .body<Unit>()
 | 
			
		||||
 | 
			
		||||
    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<Unit>()
 | 
			
		||||
 | 
			
		||||
    override suspend fun alterations(): List<AlterationJson> = client
 | 
			
		||||
| 
						 | 
				
			
			@ -74,18 +75,20 @@ class LwaClientImpl(
 | 
			
		|||
        .body()
 | 
			
		||||
 | 
			
		||||
    override suspend fun activeAlterations(
 | 
			
		||||
        prefix: Char,
 | 
			
		||||
        characterSheetId: String,
 | 
			
		||||
        instanceId: Int,
 | 
			
		||||
    ): List<String> = 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)
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -63,7 +63,7 @@ class AlterationRepository(
 | 
			
		|||
    ) {
 | 
			
		||||
        // alteration was active for the character toggle it off.
 | 
			
		||||
        store.toggleActiveAlteration(
 | 
			
		||||
            characterInstance = characterInstanceId,
 | 
			
		||||
            characterInstanceId = characterInstanceId,
 | 
			
		||||
            alterationId = alterationId,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -61,6 +61,7 @@ class AlterationStore(
 | 
			
		|||
        characterInstanceId: CharacterInstance.Id,
 | 
			
		||||
    ): List<String> {
 | 
			
		||||
        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,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,20 +21,46 @@ class CampaignRepository(
 | 
			
		|||
        store.update()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun characterInstanceFlow(
 | 
			
		||||
        id: Campaign.CharacterInstance.Id,
 | 
			
		||||
    fun instanceFlow(
 | 
			
		||||
        characterInstanceId: Campaign.CharacterInstance.Id,
 | 
			
		||||
    ): StateFlow<Campaign.CharacterInstance> {
 | 
			
		||||
        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<Campaign.CharacterInstance> {
 | 
			
		||||
        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 {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
                )
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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<String, List<FieldAlteration>> = 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,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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<String>(CHARACTER_PREFIX)?.getOrNull(0) ?: error("missing character id"),
 | 
			
		||||
                savedStateHandle.get<String>(CHARACTER_SHEET_ID) ?: error("missing character id"),
 | 
			
		||||
                savedStateHandle.get<Int>(CHARACTER_INSTANCE_ID) ?: error("missing character id"),
 | 
			
		||||
            ),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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<String>(CHARACTER_PREFIX)?.getOrNull(0) ?: error("missing character id"),
 | 
			
		||||
                savedStateHandle.get<String>(CHARACTER_SHEET_ID) ?: error("missing character id"),
 | 
			
		||||
                savedStateHandle.get<Int>(CHARACTER_INSTANCE_ID) ?: error("missing character id"),
 | 
			
		||||
            ),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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<CampaignLayoutScope> {
 | 
			
		|||
 | 
			
		||||
@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<DpSize>,
 | 
			
		||||
    val leftPanel: State<DpSize>,
 | 
			
		||||
    val rightOverlay: State<DpSize>,
 | 
			
		||||
    val rightPanel: State<DpSize>,
 | 
			
		||||
    val chatOverlay: State<DpSize>,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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(
 | 
			
		||||
| 
						 | 
				
			
			@ -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<Float, AnimationVector1D> = Animatable(0f),
 | 
			
		||||
    val rotation: Animatable<Float, AnimationVector1D> = Animatable(0f),
 | 
			
		||||
    val scale: Animatable<Float, AnimationVector1D> = 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 {
 | 
			
		||||
| 
						 | 
				
			
			@ -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<String, List<FieldAlteration>>,
 | 
			
		||||
        characterInstanceId: Campaign.CharacterInstance.Id,
 | 
			
		||||
        characterInstance: Campaign.CharacterInstance,
 | 
			
		||||
        alterations: Map<String, List<FieldAlteration>>,
 | 
			
		||||
    ): 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,
 | 
			
		||||
                )
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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<CharacterDetailPanelUio>.() -> 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<CharacterDetailPanelUio>,
 | 
			
		||||
    transitionSpec: AnimatedContentTransitionScope<CharacterDetailPanelUio>.() -> 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<CharacterDetailPanelUio>.() -> 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
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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 ->
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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<List<PlayerPortraitUio>> = campaignRepository.campaignFlow
 | 
			
		||||
        .flatMapMerge { campaign ->
 | 
			
		||||
            combine<PlayerPortraitUio?, List<PlayerPortraitUio>>(
 | 
			
		||||
                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<String, MutableState<PlayerPortraitRollUio?>>()
 | 
			
		||||
 | 
			
		||||
    @Composable
 | 
			
		||||
    @Stable
 | 
			
		||||
    fun roll(characterSheetId: String): State<PlayerPortraitRollUio?> {
 | 
			
		||||
        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
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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<CharacterId, MutableState<CharacterPortraitRollUio?>>()
 | 
			
		||||
 | 
			
		||||
    @OptIn(ExperimentalCoroutinesApi::class)
 | 
			
		||||
    val characters: StateFlow<List<CharacterPortraitUio>> = campaignRepository.campaignFlow
 | 
			
		||||
        .flatMapMerge { campaign ->
 | 
			
		||||
            if (campaign.npcs.isEmpty()) {
 | 
			
		||||
                flowOf(emptyList())
 | 
			
		||||
            } else {
 | 
			
		||||
                combine<CharacterPortraitUio?, List<CharacterPortraitUio>>(
 | 
			
		||||
                    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<CharacterPortraitRollUio?> {
 | 
			
		||||
        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
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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 = {
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -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<CharacterId, MutableState<CharacterPortraitRollUio?>>()
 | 
			
		||||
 | 
			
		||||
    @OptIn(ExperimentalCoroutinesApi::class)
 | 
			
		||||
    val characters: StateFlow<List<CharacterPortraitUio>> = campaignRepository.campaignFlow
 | 
			
		||||
        .flatMapMerge { campaign ->
 | 
			
		||||
            if (campaign.characters.isEmpty()) {
 | 
			
		||||
                flowOf(emptyList())
 | 
			
		||||
            } else {
 | 
			
		||||
                combine<CharacterPortraitUio?, List<CharacterPortraitUio>>(
 | 
			
		||||
                    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<CharacterPortraitRollUio?> {
 | 
			
		||||
        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
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
                )
 | 
			
		||||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			@ -51,7 +51,7 @@ class CharacterSheetViewModel(
 | 
			
		|||
    val diminishedDialog: State<DiminishedStatDialogUio?> get() = _diminishedDialog
 | 
			
		||||
 | 
			
		||||
    val diminishedValueFlow: StateFlow<Int?> = 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,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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<ErrorSnackUio>()
 | 
			
		||||
    val error: SharedFlow<ErrorSnackUio> = _errors
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
    )
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue