Add GM & NPC (UI) support. Change the Id system.

This commit is contained in:
Thomas Andres Gomez 2025-03-15 17:49:12 +01:00
parent 6b86a6c075
commit 27dba5438e
54 changed files with 816 additions and 426 deletions

View file

@ -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.navigation.window.rememberMaxWindowHeight
import com.pixelized.desktop.lwa.ui.overlay.roll.RollHostState import com.pixelized.desktop.lwa.ui.overlay.roll.RollHostState
import com.pixelized.desktop.lwa.ui.overlay.roll.RollOverlay 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.characterSheet.CharacterSheetMainNavHost
import com.pixelized.desktop.lwa.ui.screen.gamemaster.GameMasterScreen import com.pixelized.desktop.lwa.ui.screen.gamemaster.GameMasterScreen
import com.pixelized.desktop.lwa.ui.screen.rollhistory.RollHistoryPage import com.pixelized.desktop.lwa.ui.screen.rollhistory.RollHistoryPage
@ -108,7 +108,7 @@ fun ApplicationScope.LwaApplication() {
size = DpSize( size = DpSize(
width = 800.dp, width = 800.dp,
height = min( 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, b = maxWindowHeight,
), ),
), ),

View file

@ -43,7 +43,7 @@ class DataSyncViewModel(
.filter { status -> status == NetworkRepository.Status.CONNECTED } .filter { status -> status == NetworkRepository.Status.CONNECTED }
.combine(campaignRepository.campaignFlow) { _, campaign: Campaign -> campaign } .combine(campaignRepository.campaignFlow) { _, campaign: Campaign -> campaign }
.onEach { campaign -> .onEach { campaign ->
campaign.characters.keys.forEach { id -> (campaign.characters.keys + campaign.npcs.keys).forEach { id ->
characterRepository.characterDetail( characterRepository.characterDetail(
characterSheetId = id.characterSheetId, characterSheetId = id.characterSheetId,
forceUpdate = true, forceUpdate = true,

View file

@ -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.composable.character.characteristic.CharacterSheetCharacteristicDialogFactory
import com.pixelized.desktop.lwa.ui.overlay.roll.RollViewModel 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.toolbar.CampaignToolbarViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.chat.CampaignChatViewModel import com.pixelized.desktop.lwa.ui.screen.campaign.text.CampaignChatViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.chat.TextMessageFactory 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.CharacterDetailFactory
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailViewModel 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.detail.CharacterDiminishedViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.PlayerRibbonFactory import com.pixelized.desktop.lwa.ui.screen.campaign.player.CharacterRibbonFactory
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.PlayerRibbonViewModel 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.CharacterSheetFactory
import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.CharacterSheetViewModel import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.CharacterSheetViewModel
import com.pixelized.desktop.lwa.ui.screen.characterSheet.edit.CharacterSheetEditFactory 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.levelup.LevelUpViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.network.NetworkFactory 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.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.GameMasterActionUseCase
import com.pixelized.desktop.lwa.ui.screen.gamemaster.GameMasterFactory import com.pixelized.desktop.lwa.ui.screen.gamemaster.GameMasterFactory
import com.pixelized.desktop.lwa.ui.screen.gamemaster.GameMasterViewModel import com.pixelized.desktop.lwa.ui.screen.gamemaster.GameMasterViewModel
@ -109,7 +110,7 @@ val factoryDependencies
factoryOf(::SkillFieldFactory) factoryOf(::SkillFieldFactory)
factoryOf(::SettingsFactory) factoryOf(::SettingsFactory)
factoryOf(::CampaignJsonFactory) factoryOf(::CampaignJsonFactory)
factoryOf(::PlayerRibbonFactory) factoryOf(::CharacterRibbonFactory)
factoryOf(::CharacterDetailFactory) factoryOf(::CharacterDetailFactory)
factoryOf(::CharacterSheetCharacteristicDialogFactory) factoryOf(::CharacterSheetCharacteristicDialogFactory)
factoryOf(::TextMessageFactory) factoryOf(::TextMessageFactory)
@ -127,6 +128,7 @@ val viewModelDependencies
viewModelOf(::RollHistoryViewModel) viewModelOf(::RollHistoryViewModel)
viewModelOf(::NetworkViewModel) viewModelOf(::NetworkViewModel)
viewModelOf(::PlayerRibbonViewModel) viewModelOf(::PlayerRibbonViewModel)
viewModelOf(::NpcRibbonViewModel)
viewModelOf(::CharacterDetailViewModel) viewModelOf(::CharacterDetailViewModel)
viewModelOf(::CharacterDiminishedViewModel) viewModelOf(::CharacterDiminishedViewModel)
viewModelOf(::CharacterDetailCharacteristicDialogViewModel) viewModelOf(::CharacterDetailCharacteristicDialogViewModel)

View file

@ -13,23 +13,28 @@ interface LwaClient {
suspend fun updateCharacter(sheet: CharacterSheetJson) suspend fun updateCharacter(sheet: CharacterSheetJson)
suspend fun deleteCharacter(id: String) suspend fun deleteCharacterSheet(id: String)
suspend fun campaign(): CampaignJson suspend fun campaign(): CampaignJson
suspend fun campaignAddCharacter(characterSheetId: String) 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 campaignAddNpc(characterSheetId: String)
suspend fun campaignDeleteNpc(characterSheetId: String, instanceId: Int) suspend fun campaignRemoveNpc(characterSheetId: String, instanceId: Int)
suspend fun alterations(): List<AlterationJson> 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( suspend fun toggleActiveAlterations(
prefix: Char,
characterSheetId: String, characterSheetId: String,
instanceId: Int, instanceId: Int,
alterationId: String, alterationId: String,

View file

@ -2,6 +2,7 @@ package com.pixelized.desktop.lwa.network
import com.pixelized.desktop.lwa.repository.settings.SettingsRepository import com.pixelized.desktop.lwa.repository.settings.SettingsRepository
import com.pixelized.shared.lwa.model.alteration.AlterationJson 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.campaign.CampaignJson
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJson import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJson
import com.pixelized.shared.lwa.protocol.rest.CharacterPreviewJson import com.pixelized.shared.lwa.protocol.rest.CharacterPreviewJson
@ -25,7 +26,7 @@ class LwaClientImpl(
.body() .body()
override suspend fun character(id: String): CharacterSheetJson = client override suspend fun character(id: String): CharacterSheetJson = client
.get("$root/character/detail?id=$id") .get("$root/character/detail?characterSheetId=$id")
.body() .body()
override suspend fun updateCharacter(sheet: CharacterSheetJson) = client override suspend fun updateCharacter(sheet: CharacterSheetJson) = client
@ -35,8 +36,8 @@ class LwaClientImpl(
} }
.body<Unit>() .body<Unit>()
override suspend fun deleteCharacter(id: String) = client override suspend fun deleteCharacterSheet(id: String) = client
.delete("$root/character/delete?id=$id") .delete("$root/character/delete?characterSheetId=$id")
.body<Unit>() .body<Unit>()
override suspend fun campaign(): CampaignJson = client override suspend fun campaign(): CampaignJson = client
@ -49,11 +50,11 @@ class LwaClientImpl(
.put("$root/campaign/character/update?characterSheetId=$characterSheetId") .put("$root/campaign/character/update?characterSheetId=$characterSheetId")
.body<Unit>() .body<Unit>()
override suspend fun campaignDeleteCharacter( override suspend fun campaignRemoveCharacter(
characterSheetId: String, characterSheetId: String,
instanceId: Int, instanceId: Int,
) = client ) = 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>() .body<Unit>()
override suspend fun campaignAddNpc( override suspend fun campaignAddNpc(
@ -62,11 +63,11 @@ class LwaClientImpl(
.put("$root/campaign/npc/update?characterSheetId=$characterSheetId") .put("$root/campaign/npc/update?characterSheetId=$characterSheetId")
.body<Unit>() .body<Unit>()
override suspend fun campaignDeleteNpc( override suspend fun campaignRemoveNpc(
characterSheetId: String, characterSheetId: String,
instanceId: Int, instanceId: Int,
) = client ) = 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>() .body<Unit>()
override suspend fun alterations(): List<AlterationJson> = client override suspend fun alterations(): List<AlterationJson> = client
@ -74,18 +75,20 @@ class LwaClientImpl(
.body() .body()
override suspend fun activeAlterations( override suspend fun activeAlterations(
prefix: Char,
characterSheetId: String, characterSheetId: String,
instanceId: Int, instanceId: Int,
): List<String> = client ): List<String> = client
.get("$root/alterations/active?characterSheetId=$characterSheetId&instanceId=$instanceId") .get("$root/alterations/active?characterSheetId=$characterSheetId&instanceId=$instanceId&prefix=$prefix")
.body() .body()
override suspend fun toggleActiveAlterations( override suspend fun toggleActiveAlterations(
prefix: Char,
characterSheetId: String, characterSheetId: String,
instanceId: Int, instanceId: Int,
alterationId: String, alterationId: String,
) = client ) = 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) contentType(ContentType.Application.Json)
setBody(alterationId) setBody(alterationId)
} }

View file

@ -63,7 +63,7 @@ class AlterationRepository(
) { ) {
// alteration was active for the character toggle it off. // alteration was active for the character toggle it off.
store.toggleActiveAlteration( store.toggleActiveAlteration(
characterInstance = characterInstanceId, characterInstanceId = characterInstanceId,
alterationId = alterationId, alterationId = alterationId,
) )
} }

View file

@ -61,6 +61,7 @@ class AlterationStore(
characterInstanceId: CharacterInstance.Id, characterInstanceId: CharacterInstance.Id,
): List<String> { ): List<String> {
val request = client.activeAlterations( val request = client.activeAlterations(
prefix = characterInstanceId.prefix,
characterSheetId = characterInstanceId.characterSheetId, characterSheetId = characterInstanceId.characterSheetId,
instanceId = characterInstanceId.instanceId, instanceId = characterInstanceId.instanceId,
) )
@ -76,12 +77,13 @@ class AlterationStore(
} }
suspend fun toggleActiveAlteration( suspend fun toggleActiveAlteration(
characterInstance: CharacterInstance.Id, characterInstanceId: CharacterInstance.Id,
alterationId: String, alterationId: String,
) { ) {
client.toggleActiveAlterations( client.toggleActiveAlterations(
characterSheetId = characterInstance.characterSheetId, prefix = characterInstanceId.prefix,
instanceId = characterInstance.instanceId, characterSheetId = characterInstanceId.characterSheetId,
instanceId = characterInstanceId.instanceId,
alterationId = alterationId, alterationId = alterationId,
) )
} }

View file

@ -21,20 +21,46 @@ class CampaignRepository(
store.update() store.update()
} }
fun characterInstanceFlow( fun instanceFlow(
id: Campaign.CharacterInstance.Id, characterInstanceId: Campaign.CharacterInstance.Id,
): StateFlow<Campaign.CharacterInstance> { ): StateFlow<Campaign.CharacterInstance> {
return campaignFlow return campaignFlow
.mapNotNull { .mapNotNull {
it.characters[id] it.characters[characterInstanceId] ?: it.npcs[characterInstanceId]
} }
.stateIn( .stateIn(
scope = scope, scope = scope,
started = SharingStarted.Eagerly, 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( fun characterInstance(
characterInstanceId: Campaign.CharacterInstance.Id, characterInstanceId: Campaign.CharacterInstance.Id,
): Campaign.CharacterInstance { ): Campaign.CharacterInstance {

View file

@ -59,7 +59,7 @@ class CampaignStore(
characterSheetId: String, characterSheetId: String,
instanceId: Int, instanceId: Int,
) { ) {
client.campaignDeleteCharacter( client.campaignRemoveCharacter(
characterSheetId = characterSheetId, characterSheetId = characterSheetId,
instanceId = instanceId, instanceId = instanceId,
) )
@ -77,7 +77,7 @@ class CampaignStore(
characterSheetId: String, characterSheetId: String,
instanceId: Int, instanceId: Int,
) { ) {
client.campaignDeleteNpc( client.campaignRemoveNpc(
characterSheetId = characterSheetId, characterSheetId = characterSheetId,
instanceId = instanceId, instanceId = instanceId,
) )
@ -93,6 +93,7 @@ class CampaignStore(
is CampaignMessage -> { is CampaignMessage -> {
val instanceId = Campaign.CharacterInstance.Id( val instanceId = Campaign.CharacterInstance.Id(
prefix = payload.prefix,
characterSheetId = payload.characterSheetId, characterSheetId = payload.characterSheetId,
instanceId = payload.instanceId, instanceId = payload.instanceId,
) )

View file

@ -86,7 +86,7 @@ class CharacterSheetStore(
characterId: String, characterId: String,
) { ) {
try { try {
client.deleteCharacter(id = characterId) client.deleteCharacterSheet(id = characterId)
_detailFlow.delete(characterId = characterId) _detailFlow.delete(characterId = characterId)
} catch (exception: Exception) { } catch (exception: Exception) {
// TODO // TODO

View file

@ -41,7 +41,7 @@ class CharacterDetailCharacteristicDialogViewModel(
val sheet: CharacterSheet? = characterSheetRepository.characterDetail( val sheet: CharacterSheet? = characterSheetRepository.characterDetail(
characterSheetId = characterInstanceId.characterSheetId, characterSheetId = characterInstanceId.characterSheetId,
) )
val characterInstance: Campaign.CharacterInstance = campaignRepository.characterInstance( val characterInstance: Campaign.CharacterInstance = campaignRepository.instance(
characterInstanceId = characterInstanceId, characterInstanceId = characterInstanceId,
) )
val alterations: Map<String, List<FieldAlteration>> = alterationRepository.alterations( val alterations: Map<String, List<FieldAlteration>> = alterationRepository.alterations(
@ -80,6 +80,7 @@ class CharacterDetailCharacteristicDialogViewModel(
// share the data through the websocket. // share the data through the websocket.
network.share( network.share(
payload = CampaignMessage.UpdateCharacteristic( payload = CampaignMessage.UpdateCharacteristic(
prefix = characterInstanceId.prefix,
characterSheetId = characterInstanceId.characterSheetId, characterSheetId = characterInstanceId.characterSheetId,
instanceId = characterInstanceId.instanceId, instanceId = characterInstanceId.instanceId,
characteristic = characteristicJson, characteristic = characteristicJson,

View file

@ -14,12 +14,17 @@ object CharacterSheetDestination {
private const val ROUTE = "character.sheet" private const val ROUTE = "character.sheet"
private const val CHARACTER_SHEET_ID = "sheetId" private const val CHARACTER_SHEET_ID = "sheetId"
private const val CHARACTER_INSTANCE_ID = "instanceId" 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 + fun navigationRoute(characterInstanceId: Campaign.CharacterInstance.Id) = ROUTE +
"?$CHARACTER_SHEET_ID=${characterInstanceId.characterSheetId}" + "?$CHARACTER_PREFIX=${characterInstanceId.prefix}" +
"&$CHARACTER_INSTANCE_ID=${characterInstanceId.instanceId}" "&$CHARACTER_INSTANCE_ID=${characterInstanceId.instanceId}" +
"&$CHARACTER_SHEET_ID=${characterInstanceId.characterSheetId}"
fun arguments() = listOf( fun arguments() = listOf(
navArgument(CHARACTER_SHEET_ID) { navArgument(CHARACTER_SHEET_ID) {
@ -30,6 +35,10 @@ object CharacterSheetDestination {
nullable = false nullable = false
type = NavType.IntType type = NavType.IntType
}, },
navArgument(CHARACTER_PREFIX) {
nullable = false
type = NavType.StringType
}
) )
data class Argument( data class Argument(
@ -37,6 +46,7 @@ object CharacterSheetDestination {
) { ) {
constructor(savedStateHandle: SavedStateHandle) : this( constructor(savedStateHandle: SavedStateHandle) : this(
characterInstanceId = Campaign.CharacterInstance.Id( 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<String>(CHARACTER_SHEET_ID) ?: error("missing character id"),
savedStateHandle.get<Int>(CHARACTER_INSTANCE_ID) ?: error("missing character id"), savedStateHandle.get<Int>(CHARACTER_INSTANCE_ID) ?: error("missing character id"),
), ),

View file

@ -12,14 +12,20 @@ import com.pixelized.shared.lwa.model.campaign.Campaign
object LevelUpDestination { object LevelUpDestination {
private const val ROUTE = "levelUp" private const val ROUTE = "levelUp"
private const val CHARACTER_SHEET_ID = "sheetId" private const val CHARACTER_SHEET_ID = "sheetId"
private const val CHARACTER_INSTANCE_ID = "instanceId" 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 + fun navigationRoute(characterInstanceId: Campaign.CharacterInstance.Id) = ROUTE +
"?$CHARACTER_SHEET_ID=${characterInstanceId.characterSheetId}" + "?$CHARACTER_PREFIX=${characterInstanceId.prefix}" +
"&$CHARACTER_INSTANCE_ID=${characterInstanceId.instanceId}" "&$CHARACTER_INSTANCE_ID=${characterInstanceId.instanceId}" +
"&$CHARACTER_SHEET_ID=${characterInstanceId.characterSheetId}"
fun arguments() = listOf( fun arguments() = listOf(
navArgument(CHARACTER_SHEET_ID) { navArgument(CHARACTER_SHEET_ID) {
@ -30,6 +36,10 @@ object LevelUpDestination {
nullable = false nullable = false
type = NavType.IntType type = NavType.IntType
}, },
navArgument(CHARACTER_PREFIX) {
nullable = false
type = NavType.StringType
}
) )
data class Argument( data class Argument(
@ -37,6 +47,7 @@ object LevelUpDestination {
) { ) {
constructor(savedStateHandle: SavedStateHandle) : this( constructor(savedStateHandle: SavedStateHandle) : this(
characterInstanceId = Campaign.CharacterInstance.Id( 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<String>(CHARACTER_SHEET_ID) ?: error("missing character id"),
savedStateHandle.get<Int>(CHARACTER_INSTANCE_ID) ?: error("missing character id"), savedStateHandle.get<Int>(CHARACTER_INSTANCE_ID) ?: error("missing character id"),
), ),

View file

@ -41,8 +41,7 @@ class RollViewModel(
private val alteredCharacterSheetFactory: AlteredCharacterSheetFactory, private val alteredCharacterSheetFactory: AlteredCharacterSheetFactory,
) : ViewModel() { ) : ViewModel() {
private var alteredCharacterSheet: AlteredCharacterSheet? = null private var alteredCharacterSheet: AlteredCharacterSheet? = null
private var rollAction: String? = null private var rollAction: RollActionUio? = null
private var rollSuccessValue: Int? = null
var lastRollResult: RollResult = RollResult.Dismissed var lastRollResult: RollResult = RollResult.Dismissed
private set private set
@ -67,7 +66,6 @@ class RollViewModel(
suspend fun cleanRoll() { suspend fun cleanRoll() {
alteredCharacterSheet = null alteredCharacterSheet = null
rollAction = null rollAction = null
rollSuccessValue = null
lastRollResult = RollResult.Dismissed lastRollResult = RollResult.Dismissed
@ -106,10 +104,9 @@ class RollViewModel(
alterations = alterations, alterations = alterations,
) )
this.rollAction = roll.rollAction this.rollAction = roll
this.rollSuccessValue = roll.rollSuccessValue
val rollStep = rollSuccessValue?.let { val rollStep = roll.rollSuccessValue?.let {
skillStepUseCase.computeSkillStep(skill = it) skillStepUseCase.computeSkillStep(skill = it)
} }
@ -119,7 +116,7 @@ class RollViewModel(
label = roll.label, label = roll.label,
value = rollStep?.success?.last value = rollStep?.success?.last
) )
_rollDifficulty.value = rollSuccessValue?.let { _rollDifficulty.value = roll.rollSuccessValue?.let {
DifficultyUio( DifficultyUio(
open = false, open = false,
difficulty = Difficulty.NORMAL, difficulty = Difficulty.NORMAL,
@ -148,7 +145,7 @@ class RollViewModel(
delay(500) delay(500)
_cancellable.value = false _cancellable.value = false
// compute the skill critical success to critical failure ranges. // compute the skill critical success to critical failure ranges.
val rollStep = rollSuccessValue?.let { val rollStep = rollAction.rollSuccessValue?.let {
skillStepUseCase.computeSkillStep( skillStepUseCase.computeSkillStep(
skill = when (_rollDifficulty.value?.difficulty) { skill = when (_rollDifficulty.value?.difficulty) {
Difficulty.EASY -> it * 2 Difficulty.EASY -> it * 2
@ -163,7 +160,7 @@ class RollViewModel(
// compute the roll (typically use the expression inside the rollAction) // compute the roll (typically use the expression inside the rollAction)
val roll = skillComputation.computeRoll( val roll = skillComputation.computeRoll(
sheet = alteredCharacterSheet, sheet = alteredCharacterSheet,
expression = rollAction, expression = rollAction.rollAction,
) )
// check where the roll fall into the rollSteps. // check where the roll fall into the rollSteps.
@ -196,7 +193,6 @@ class RollViewModel(
launch { launch {
shareRollResult( shareRollResult(
alteredCharacterSheet = alteredCharacterSheet,
rollTitle = rollTitle, rollTitle = rollTitle,
roll = roll, roll = roll,
rollStep = rollStep, rollStep = rollStep,
@ -219,7 +215,7 @@ class RollViewModel(
open = false, open = false,
difficulty = difficulty, difficulty = difficulty,
) )
val rollStep = rollSuccessValue?.let { val rollStep = rollAction?.rollSuccessValue?.let {
skillStepUseCase.computeSkillStep( skillStepUseCase.computeSkillStep(
skill = when (_rollDifficulty.value?.difficulty) { skill = when (_rollDifficulty.value?.difficulty) {
Difficulty.EASY -> it * 2 Difficulty.EASY -> it * 2
@ -263,15 +259,18 @@ class RollViewModel(
} }
private suspend fun shareRollResult( private suspend fun shareRollResult(
alteredCharacterSheet: AlteredCharacterSheet,
rollTitle: RollTitleUio, rollTitle: RollTitleUio,
roll: Int, roll: Int,
rollStep: SkillStepUseCase.SkillStep?, rollStep: SkillStepUseCase.SkillStep?,
success: String?, success: String?,
) { ) {
val rollAction = rollAction ?: return
val payload = RollMessage( val payload = RollMessage(
id = RollMessage.RollId.create(), id = RollMessage.RollId.create(),
characterSheetId = alteredCharacterSheet.id, prefix = rollAction.characterInstanceId.prefix,
characterSheetId = rollAction.characterInstanceId.characterSheetId,
instanceId = rollAction.characterInstanceId.instanceId,
skillLabel = rollTitle.label, skillLabel = rollTitle.label,
rollValue = roll, rollValue = roll,
resultLabel = success, resultLabel = success,

View file

@ -26,6 +26,7 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.composable.blur.BlurContent import com.pixelized.desktop.lwa.ui.composable.blur.BlurContent
import com.pixelized.desktop.lwa.ui.composable.blur.rememberBlurContentController 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.composable.key.KeyHandler
import com.pixelized.desktop.lwa.ui.navigation.screen.LocalScreenController 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.navigation.screen.destination.navigateToLevelScreen
import com.pixelized.desktop.lwa.ui.screen.campaign.chat.CampaignChat import com.pixelized.desktop.lwa.ui.screen.campaign.text.CampaignChat
import com.pixelized.desktop.lwa.ui.screen.campaign.chat.CampaignChatViewModel 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.CharacterDetailPanel
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailViewModel 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.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.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.campaign.toolbar.CampaignToolbarViewModel
import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.dialog.DiminishedStatDialog
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.compose.viewmodel.koinViewModel import org.koin.compose.viewmodel.koinViewModel
@ -53,11 +55,11 @@ val LocalCampaignLayoutScope = compositionLocalOf<CampaignLayoutScope> {
@Composable @Composable
fun CampaignScreen( fun CampaignScreen(
characterDetailViewModel: CharacterDetailViewModel = koinViewModel(), playerDetailViewModel: CharacterDetailViewModel = koinViewModel(key = "player"),
npcDetailViewModel: CharacterDetailViewModel = koinViewModel(key = "npc"),
characteristicDialogViewModel: CharacterDetailCharacteristicDialogViewModel = koinViewModel(), characteristicDialogViewModel: CharacterDetailCharacteristicDialogViewModel = koinViewModel(),
dismissedViewModel: CharacterDiminishedViewModel = koinViewModel(), dismissedViewModel: CharacterDiminishedViewModel = koinViewModel(),
campaignViewModel: CampaignToolbarViewModel = koinViewModel(), campaignViewModel: CampaignToolbarViewModel = koinViewModel(),
networkViewModel: NetworkViewModel = koinViewModel(),
campaignChatViewModel: CampaignChatViewModel = koinViewModel(), campaignChatViewModel: CampaignChatViewModel = koinViewModel(),
) { ) {
val screen = LocalScreenController.current val screen = LocalScreenController.current
@ -67,7 +69,8 @@ fun CampaignScreen(
KeyHandler { KeyHandler {
when { when {
it.type == KeyEventType.KeyUp && it.key == Key.Escape -> { it.type == KeyEventType.KeyUp && it.key == Key.Escape -> {
characterDetailViewModel.hideCharacter() playerDetailViewModel.hideCharacter()
npcDetailViewModel.hideCharacter()
true true
} }
@ -82,7 +85,7 @@ fun CampaignScreen(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
controller = blurController controller = blurController
) { ) {
CampaignScreenLayout( CampaignLayout(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
top = { top = {
CampaignToolbar( CampaignToolbar(
@ -101,25 +104,50 @@ fun CampaignScreen(
chatViewModel = campaignChatViewModel, chatViewModel = campaignChatViewModel,
) )
}, },
leftOverlay = { leftPanel = {
PlayerRibbon( PlayerRibbon(
modifier = Modifier.fillMaxHeight(), modifier = Modifier.fillMaxHeight(),
onCharacter = { onCharacter = {
characterDetailViewModel.showCharacter(id = it) playerDetailViewModel.showCharacter(id = it)
}, },
onLevelUp = { onLevelUp = {
screen.navigateToLevelScreen(characterInstanceId = it) 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 = { rightOverlay = {
CharacterDetailPanel( CharacterDetailPanel(
modifier = Modifier modifier = Modifier
.padding(all = 8.dp) .padding(all = 8.dp)
.width(width = 128.dp * 4) .width(width = 128.dp * 4)
.fillMaxHeight(), .fillMaxHeight(),
transitionSpec = rememberTransitionAnimation(direction = LayoutDirection.Rtl),
blurController = blurController, blurController = blurController,
detailViewModel = characterDetailViewModel, detailViewModel = playerDetailViewModel,
characterDiminishedViewModel = dismissedViewModel, characterDiminishedViewModel = dismissedViewModel,
characteristicDialogViewModel = characteristicDialogViewModel, characteristicDialogViewModel = characteristicDialogViewModel,
) )
@ -166,26 +194,34 @@ fun CampaignScreen(
} }
@Composable @Composable
private fun CampaignScreenLayout( private fun CampaignLayout(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
top: @Composable () -> Unit, top: @Composable () -> Unit,
bottom: @Composable () -> Unit, bottom: @Composable () -> Unit,
main: @Composable () -> Unit, main: @Composable () -> Unit,
chat: @Composable () -> Unit, chat: @Composable () -> Unit,
leftPanel: @Composable () -> Unit,
rightPanel: @Composable () -> Unit,
leftOverlay: @Composable () -> Unit, leftOverlay: @Composable () -> Unit,
rightOverlay: @Composable () -> Unit, rightOverlay: @Composable () -> Unit,
) { ) {
val density = LocalDensity.current val density = LocalDensity.current
val leftOverlayState = remember { mutableStateOf(DpSize.Unspecified) } val leftOverlayState = remember { mutableStateOf(DpSize.Unspecified) }
val leftPanelState = remember { mutableStateOf(DpSize.Unspecified) }
val rightOverlayState = remember { mutableStateOf(DpSize.Unspecified) } val rightOverlayState = remember { mutableStateOf(DpSize.Unspecified) }
val rightPanelState = remember { mutableStateOf(DpSize.Unspecified) }
val chatOverlayState = remember { mutableStateOf(DpSize.Unspecified) } val chatOverlayState = remember { mutableStateOf(DpSize.Unspecified) }
val scope = remember { val scope = remember {
CampaignLayoutScope( CampaignLayoutScope(
leftOverlay = leftOverlayState, leftOverlay = leftOverlayState,
leftPanel = leftPanelState,
rightOverlay = rightOverlayState, rightOverlay = rightOverlayState,
rightPanel = rightPanelState,
chatOverlay = chatOverlayState, chatOverlay = chatOverlayState,
) )
} }
CompositionLocalProvider( CompositionLocalProvider(
LocalCampaignLayoutScope provides scope, LocalCampaignLayoutScope provides scope,
) { ) {
@ -194,7 +230,7 @@ private fun CampaignScreenLayout(
) { ) {
top() top()
Box( Box(
modifier = Modifier.weight(1f, fill = true), modifier = Modifier.weight(weight = 1f, fill = true),
) { ) {
Box( Box(
modifier = Modifier modifier = Modifier
@ -210,6 +246,13 @@ private fun CampaignScreenLayout(
) { ) {
chat() chat()
} }
Box(
modifier = Modifier
.align(alignment = Alignment.CenterStart)
.onSizeChanged { leftPanelState.value = it.toDp(density) },
) {
leftPanel()
}
Box( Box(
modifier = Modifier modifier = Modifier
.align(alignment = Alignment.CenterStart) .align(alignment = Alignment.CenterStart)
@ -217,6 +260,13 @@ private fun CampaignScreenLayout(
) { ) {
leftOverlay() leftOverlay()
} }
Box(
modifier = Modifier
.align(alignment = Alignment.CenterEnd)
.onSizeChanged { rightPanelState.value = it.toDp(density) },
) {
rightPanel()
}
Box( Box(
modifier = Modifier modifier = Modifier
.align(alignment = Alignment.CenterEnd) .align(alignment = Alignment.CenterEnd)
@ -233,7 +283,9 @@ private fun CampaignScreenLayout(
@Stable @Stable
data class CampaignLayoutScope( data class CampaignLayoutScope(
val leftOverlay: State<DpSize>, val leftOverlay: State<DpSize>,
val leftPanel: State<DpSize>,
val rightOverlay: State<DpSize>, val rightOverlay: State<DpSize>,
val rightPanel: State<DpSize>,
val chatOverlay: State<DpSize>, val chatOverlay: State<DpSize>,
) )

View file

@ -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.AnimatedContent
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
@ -53,22 +53,33 @@ import kotlin.math.max
import kotlin.math.min import kotlin.math.min
@Stable @Stable
data class PlayerPortraitUio( object CharacterPortraitDefault {
val size = DpSize(96.dp, 128.dp)
}
@Stable
data class CharacterPortraitUio(
val id: Campaign.CharacterInstance.Id, val id: Campaign.CharacterInstance.Id,
val portrait: String?, val portrait: String?,
val name: String, val name: String,
val hp: Int,
val maxHp: Int,
val pp: Int,
val maxPp: Int,
val levelUp: Boolean, 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 @Composable
fun PlayerPortrait( fun CharacterPortrait(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
size: DpSize, size: DpSize = CharacterPortraitDefault.size,
character: PlayerPortraitUio, character: CharacterPortraitUio,
onCharacter: (id: Campaign.CharacterInstance.Id) -> Unit, onCharacter: (id: Campaign.CharacterInstance.Id) -> Unit,
onLevelUp: (id: Campaign.CharacterInstance.Id) -> Unit, onLevelUp: (id: Campaign.CharacterInstance.Id) -> Unit,
) { ) {
@ -77,9 +88,9 @@ fun PlayerPortrait(
Box( Box(
modifier = modifier modifier = modifier
.size(size = size) .size(size = size)
.clip(shape = remember { RoundedCornerShape(8.dp) }) .clip(shape = MaterialTheme.lwa.shapes.portrait)
.background(color = colorScheme.elevated.base1dp) .background(color = colorScheme.elevated.base1dp)
.clickable { onCharacter(character.id) }, .clickable(character.enableDetail) { onCharacter(character.id) },
) { ) {
AnimatedContent( AnimatedContent(
targetState = character.portrait, targetState = character.portrait,
@ -95,11 +106,6 @@ fun PlayerPortrait(
) )
} }
BloodOverlay(
maxHp = character.maxHp.toFloat(),
hp = character.hp.toFloat(),
)
AnimatedVisibility( AnimatedVisibility(
modifier = Modifier.offset(x = (-8).dp, y = (-8).dp), modifier = Modifier.offset(x = (-8).dp, y = (-8).dp),
visible = character.levelUp, visible = character.levelUp,
@ -115,59 +121,67 @@ fun PlayerPortrait(
} }
} }
Column( character.stats?.let { stats ->
modifier = Modifier
.fillMaxSize() BloodOverlay(
.drawWithContent { maxHp = stats.maxHp.toFloat(),
drawRect(brush = colorScheme.portraitBackgroundBrush) hp = stats.hp.toFloat(),
drawContent() )
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)), targetValue = min(maxHp, max(0f, (maxHp - hp) / maxHp)),
animationSpec = tween(durationMillis = 350, easing = EaseOutCirc) animationSpec = tween(durationMillis = 350, easing = EaseOutCirc)
) )
val animatedColor = animateColorAsState( val animatedColor = animateColorAsState(
targetValue = bloodColor.copy(alpha = ((maxHp - hp) / maxHp) / 4f + .25f) targetValue = bloodColor.copy(alpha = ((maxHp - hp) / maxHp) / 4f + .25f)
) )
Box( Box(

View file

@ -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.AnimatedContent
import androidx.compose.animation.SizeTransform 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.text.style.TextAlign
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.pixelized.shared.lwa.model.campaign.Campaign
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.ic_d20_24dp import lwacharactersheet.composeapp.generated.resources.ic_d20_24dp
import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.painterResource
@Stable @Stable
data class PlayerPortraitRollUio( data class CharacterPortraitRollUio(
val characterId: String, val characterId: Campaign.CharacterInstance.Id,
val value: Int?, val value: Int?,
val label: String?, val label: String?,
) )
@Stable @Stable
data class PlayerPortraitRollAnimation( data class CharacterPortraitRollAnimation(
val alpha: Animatable<Float, AnimationVector1D> = Animatable(0f), val alpha: Animatable<Float, AnimationVector1D> = Animatable(0f),
val rotation: Animatable<Float, AnimationVector1D> = Animatable(0f), val rotation: Animatable<Float, AnimationVector1D> = Animatable(0f),
val scale: Animatable<Float, AnimationVector1D> = Animatable(1f), val scale: Animatable<Float, AnimationVector1D> = Animatable(1f),
@ -58,12 +59,12 @@ data class PlayerPortraitRollAnimation(
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun PlayerPortraitRoll( fun CharacterPortraitRoll(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
size: DpSize, size: DpSize = CharacterPortraitDefault.size,
value: PlayerPortraitRollUio?, value: CharacterPortraitRollUio?,
onLeftClick: (PlayerPortraitRollUio) -> Unit, onLeftClick: (CharacterPortraitRollUio) -> Unit,
onRightClick: (PlayerPortraitRollUio) -> Unit, onRightClick: (CharacterPortraitRollUio) -> Unit,
) { ) {
AnimatedContent( AnimatedContent(
modifier = modifier modifier = modifier
@ -133,9 +134,9 @@ fun PlayerPortraitRoll(
} }
@Composable @Composable
private fun diceIconAnimation(key: Any = Unit): PlayerPortraitRollAnimation { private fun diceIconAnimation(key: Any = Unit): CharacterPortraitRollAnimation {
val animation = remember(key) { val animation = remember(key) {
PlayerPortraitRollAnimation() CharacterPortraitRollAnimation()
} }
LaunchedEffect(key) { LaunchedEffect(key) {
launch { launch {

View file

@ -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.AlteredCharacterSheetFactory
import com.pixelized.shared.lwa.model.alteration.FieldAlteration 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.campaign.power
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet
class PlayerRibbonFactory( class CharacterRibbonFactory(
private val alteredCharacterSheetFactory: AlteredCharacterSheetFactory, private val alteredCharacterSheetFactory: AlteredCharacterSheetFactory,
) { ) {
fun convertToPlayerPortraitUio( fun convertToPlayerPortraitUio(
characterSheet: CharacterSheet?, characterSheet: CharacterSheet?,
alterations: Map<String, List<FieldAlteration>>,
characterInstanceId: Campaign.CharacterInstance.Id, characterInstanceId: Campaign.CharacterInstance.Id,
characterInstance: Campaign.CharacterInstance, characterInstance: Campaign.CharacterInstance,
alterations: Map<String, List<FieldAlteration>>, enableDetail: Boolean,
): PlayerPortraitUio? { displayCharacterStats: Boolean,
): CharacterPortraitUio? {
if (characterSheet == null) return null if (characterSheet == null) return null
val alteredCharacterSheet = alteredCharacterSheetFactory.sheet( val alteredCharacterSheet = alteredCharacterSheetFactory.sheet(
@ -23,15 +25,20 @@ class PlayerRibbonFactory(
alterations = alterations, alterations = alterations,
) )
return PlayerPortraitUio( return CharacterPortraitUio(
id = characterInstanceId, id = characterInstanceId,
portrait = alteredCharacterSheet.thumbnail, portrait = alteredCharacterSheet.thumbnail,
name = alteredCharacterSheet.name, name = alteredCharacterSheet.name,
hp = alteredCharacterSheet.maxHp - characterInstance.damage,
maxHp = alteredCharacterSheet.maxHp,
pp = alteredCharacterSheet.maxPp - characterInstance.power,
maxPp = alteredCharacterSheet.maxPp,
levelUp = alteredCharacterSheet.shouldLevelUp, 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,
)
},
) )
} }
} }

View file

@ -1,6 +1,8 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.player.detail package com.pixelized.desktop.lwa.ui.screen.campaign.player.detail
import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.ContentTransform
import androidx.compose.animation.EnterTransition import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition import androidx.compose.animation.ExitTransition
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
@ -22,9 +24,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.LocalRollHostState import com.pixelized.desktop.lwa.LocalRollHostState
import com.pixelized.desktop.lwa.ui.composable.blur.BlurContentController import com.pixelized.desktop.lwa.ui.composable.blur.BlurContentController
@ -52,6 +56,7 @@ data class CharacterDetailPanelUio(
fun CharacterDetailPanel( fun CharacterDetailPanel(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
blurController: BlurContentController, blurController: BlurContentController,
transitionSpec: AnimatedContentTransitionScope<CharacterDetailPanelUio>.() -> ContentTransform = rememberTransitionAnimation(),
detailViewModel: CharacterDetailViewModel, detailViewModel: CharacterDetailViewModel,
characteristicDialogViewModel: CharacterDetailCharacteristicDialogViewModel, characteristicDialogViewModel: CharacterDetailCharacteristicDialogViewModel,
characterDiminishedViewModel: CharacterDiminishedViewModel, characterDiminishedViewModel: CharacterDiminishedViewModel,
@ -63,6 +68,7 @@ fun CharacterDetailPanel(
CharacterDetailAnimatedPanel( CharacterDetailAnimatedPanel(
modifier = modifier, modifier = modifier,
detail = detail, detail = detail,
transitionSpec = transitionSpec,
onDismissRequest = { onDismissRequest = {
detailViewModel.hideCharacter() detailViewModel.hideCharacter()
}, },
@ -125,6 +131,7 @@ fun CharacterDetailPanel(
fun CharacterDetailAnimatedPanel( fun CharacterDetailAnimatedPanel(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
detail: State<CharacterDetailPanelUio>, detail: State<CharacterDetailPanelUio>,
transitionSpec: AnimatedContentTransitionScope<CharacterDetailPanelUio>.() -> ContentTransform,
onDismissRequest: (id: Campaign.CharacterInstance.Id) -> Unit, onDismissRequest: (id: Campaign.CharacterInstance.Id) -> Unit,
onDiminished: (id: Campaign.CharacterInstance.Id) -> Unit, onDiminished: (id: Campaign.CharacterInstance.Id) -> Unit,
onHp: (id: Campaign.CharacterInstance.Id) -> Unit, onHp: (id: Campaign.CharacterInstance.Id) -> Unit,
@ -140,15 +147,7 @@ fun CharacterDetailAnimatedPanel(
AnimatedContent( AnimatedContent(
modifier = Modifier.matchParentSize(), modifier = Modifier.matchParentSize(),
targetState = detail.value, targetState = detail.value,
transitionSpec = { transitionSpec = 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
}
}
) { ) {
when { when {
it.characterInstanceId == null -> Box( 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
}
}
}
} }

View file

@ -31,7 +31,7 @@ class CharacterDetailViewModel(
CharacterDetailPanelUio( CharacterDetailPanelUio(
characterInstanceId = characterInstanceId, characterInstanceId = characterInstanceId,
header = combine( header = combine(
campaignRepository.characterInstanceFlow(id = characterInstanceId), campaignRepository.instanceFlow(characterInstanceId = characterInstanceId),
characterSheetRepository.characterDetailFlow(characterSheetId = characterInstanceId.characterSheetId), characterSheetRepository.characterDetailFlow(characterSheetId = characterInstanceId.characterSheetId),
alterationRepository.alterationsFlow(characterInstanceId = characterInstanceId), alterationRepository.alterationsFlow(characterInstanceId = characterInstanceId),
) { characterInstance, characterSheet, alterations -> ) { characterInstance, characterSheet, alterations ->
@ -47,7 +47,7 @@ class CharacterDetailViewModel(
initialValue = null, initialValue = null,
), ),
sheet = combine( sheet = combine(
campaignRepository.characterInstanceFlow(id = characterInstanceId), campaignRepository.instanceFlow(characterInstanceId = characterInstanceId),
characterSheetRepository.characterDetailFlow(characterSheetId = characterInstanceId.characterSheetId), characterSheetRepository.characterDetailFlow(characterSheetId = characterInstanceId.characterSheetId),
alterationRepository.alterationsFlow(characterInstanceId = characterInstanceId), alterationRepository.alterationsFlow(characterInstanceId = characterInstanceId),
) { characterInstance, characterSheet, alterations -> ) { characterInstance, characterSheet, alterations ->

View file

@ -57,6 +57,7 @@ class CharacterDiminishedViewModel(
val diminished = dialog.value().text.toIntOrNull() ?: 0 val diminished = dialog.value().text.toIntOrNull() ?: 0
networkRepository.share( networkRepository.share(
payload = CampaignMessage.UpdateDiminished( payload = CampaignMessage.UpdateDiminished(
prefix = dialog.characterInstanceId.prefix,
characterSheetId = dialog.characterInstanceId.characterSheetId, characterSheetId = dialog.characterInstanceId.characterSheetId,
instanceId = dialog.characterInstanceId.instanceId, instanceId = dialog.characterInstanceId.instanceId,
diminished = diminished, diminished = diminished,

View file

@ -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
}
}

View file

@ -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,
)
}
}
}
}

View file

@ -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
}
}

View file

@ -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.Arrangement
import androidx.compose.foundation.layout.PaddingValues 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.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp 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 com.pixelized.shared.lwa.model.campaign.Campaign
import org.koin.compose.viewmodel.koinViewModel import org.koin.compose.viewmodel.koinViewModel
object PlayerRibbon {
object Default {
val size = DpSize(96.dp, 128.dp)
}
}
@Composable @Composable
fun PlayerRibbon( fun PlayerRibbon(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
playerRibbonViewModel: PlayerRibbonViewModel = koinViewModel(), viewModel: PlayerRibbonViewModel = koinViewModel(),
padding: PaddingValues = PaddingValues(all = 8.dp), padding: PaddingValues = PaddingValues(all = 8.dp),
onCharacter: (id: Campaign.CharacterInstance.Id) -> Unit, onCharacter: (id: Campaign.CharacterInstance.Id) -> Unit,
onLevelUp: (id: Campaign.CharacterInstance.Id) -> Unit, onLevelUp: (id: Campaign.CharacterInstance.Id) -> Unit,
) { ) {
val characters = playerRibbonViewModel.characters.collectAsState() val characters = viewModel.characters.collectAsState()
LazyColumn( LazyColumn(
modifier = modifier, modifier = modifier,
@ -39,17 +34,15 @@ fun PlayerRibbon(
key = { it.id }, key = { it.id },
) { ) {
Row { Row {
PlayerPortrait( CharacterPortrait(
size = PlayerRibbon.Default.size,
character = it, character = it,
onCharacter = onCharacter, onCharacter = onCharacter,
onLevelUp = onLevelUp, onLevelUp = onLevelUp,
) )
PlayerPortraitRoll( CharacterPortraitRoll(
size = PlayerRibbon.Default.size, value = viewModel.roll(characterId = it.id).value,
value = playerRibbonViewModel.roll(characterSheetId = it.id.characterSheetId).value,
onRightClick = { onRightClick = {
playerRibbonViewModel.onPortraitRollRightClick(characterId = it.characterId) viewModel.onPortraitRollRightClick(characterId = it.characterId)
}, },
onLeftClick = { onLeftClick = {

View file

@ -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
}
}

View file

@ -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.animation.core.animateDpAsState
import androidx.compose.foundation.background 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.navigation.window.LocalWindowState
import com.pixelized.desktop.lwa.ui.screen.campaign.CampaignLayoutScope 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.LocalCampaignLayoutScope
import com.pixelized.desktop.lwa.ui.screen.campaign.chat.text.CharacteristicTextMessage import com.pixelized.desktop.lwa.ui.screen.campaign.player.CharacterPortraitDefault
import com.pixelized.desktop.lwa.ui.screen.campaign.chat.text.CharacteristicTextMessageUio import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.CharacteristicTextMessage
import com.pixelized.desktop.lwa.ui.screen.campaign.chat.text.DiminishedTextMessage import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.CharacteristicTextMessageUio
import com.pixelized.desktop.lwa.ui.screen.campaign.chat.text.DiminishedTextMessageUio import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.DiminishedTextMessage
import com.pixelized.desktop.lwa.ui.screen.campaign.chat.text.RollTextMessage import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.DiminishedTextMessageUio
import com.pixelized.desktop.lwa.ui.screen.campaign.chat.text.RollTextMessageUio import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.RollTextMessage
import com.pixelized.desktop.lwa.ui.screen.campaign.chat.text.TextMessage import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.RollTextMessageUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.PlayerRibbon import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.TextMessage
import com.pixelized.desktop.lwa.ui.theme.lwa import com.pixelized.desktop.lwa.ui.theme.lwa
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.compose.viewmodel.koinViewModel import org.koin.compose.viewmodel.koinViewModel
@ -73,7 +73,7 @@ fun CampaignChat(
modifier = modifier modifier = modifier
.size( .size(
width = animatedChatWidth.value, width = animatedChatWidth.value,
height = PlayerRibbon.Default.size.height * 2 + 8.dp, height = CharacterPortraitDefault.size.height * 2 + 8.dp,
) )
.graphicsLayer { .graphicsLayer {
alpha = chatViewModel.chatAnimatedVisibility.value alpha = chatViewModel.chatAnimatedVisibility.value
@ -154,7 +154,7 @@ private fun rememberAnimatedChatWidth(
val maxChatWidth = 64.dp * 12 val maxChatWidth = 64.dp * 12
val windowWidth = windowsState.size.width val windowWidth = windowsState.size.width
if (windowWidth != Dp.Unspecified) { 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) min(max(width, minChatWidth), maxChatWidth)
} else { } else {
minChatWidth minChatWidth

View file

@ -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.Animatable
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
@ -6,7 +6,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.pixelized.desktop.lwa.repository.network.NetworkRepository import com.pixelized.desktop.lwa.repository.network.NetworkRepository
import com.pixelized.desktop.lwa.repository.settings.SettingsRepository import com.pixelized.desktop.lwa.repository.settings.SettingsRepository
import com.pixelized.desktop.lwa.ui.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.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.mapNotNull

View file

@ -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.alteration.AlterationRepository
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository 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.text.messages.CharacteristicTextMessageUio
import com.pixelized.desktop.lwa.ui.screen.campaign.chat.text.DiminishedTextMessageUio import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.DiminishedTextMessageUio
import com.pixelized.desktop.lwa.ui.screen.campaign.chat.text.RollTextMessageUio import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.RollTextMessageUio
import com.pixelized.desktop.lwa.ui.screen.campaign.chat.text.TextMessage import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.TextMessage
import com.pixelized.shared.lwa.model.AlteredCharacterSheetFactory import com.pixelized.shared.lwa.model.AlteredCharacterSheetFactory
import com.pixelized.shared.lwa.model.campaign.Campaign import com.pixelized.shared.lwa.model.campaign.Campaign
import com.pixelized.shared.lwa.model.campaign.CampaignJsonV1.CharacterInstanceJsonV1.CharacteristicV1.Damage import com.pixelized.shared.lwa.model.campaign.CampaignJsonV1.CharacterInstanceJsonV1.CharacteristicV1.Damage
@ -79,6 +79,7 @@ class TextMessageFactory(
characterSheetId = payload.characterSheetId, characterSheetId = payload.characterSheetId,
) ?: return null ) ?: return null
val characterInstanceId = Campaign.CharacterInstance.Id( val characterInstanceId = Campaign.CharacterInstance.Id(
prefix = payload.prefix,
characterSheetId = payload.characterSheetId, characterSheetId = payload.characterSheetId,
instanceId = payload.instanceId, instanceId = payload.instanceId,
) )

View file

@ -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.Arrangement
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row

View file

@ -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.Arrangement
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row

View file

@ -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.Arrangement
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row

View file

@ -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 { sealed interface TextMessage {
val id : String val id : String

View file

@ -51,7 +51,7 @@ class CharacterSheetViewModel(
val diminishedDialog: State<DiminishedStatDialogUio?> get() = _diminishedDialog val diminishedDialog: State<DiminishedStatDialogUio?> get() = _diminishedDialog
val diminishedValueFlow: StateFlow<Int?> = campaignRepository val diminishedValueFlow: StateFlow<Int?> = campaignRepository
.characterInstanceFlow(id = argument.characterInstanceId) .characterInstanceFlow(characterInstanceId = argument.characterInstanceId)
.map { instance -> instance.diminished.takeIf { it > 0 } } .map { instance -> instance.diminished.takeIf { it > 0 } }
.stateIn(scope = viewModelScope, SharingStarted.Lazily, null) .stateIn(scope = viewModelScope, SharingStarted.Lazily, null)
@ -144,6 +144,7 @@ class CharacterSheetViewModel(
val diminished = dialog.value().text.toIntOrNull() ?: 0 val diminished = dialog.value().text.toIntOrNull() ?: 0
network.share( network.share(
payload = CampaignMessage.UpdateDiminished( payload = CampaignMessage.UpdateDiminished(
prefix = dialog.characterInstanceId.prefix,
characterSheetId = dialog.characterInstanceId.characterSheetId, characterSheetId = dialog.characterInstanceId.characterSheetId,
instanceId = dialog.characterInstanceId.instanceId, instanceId = dialog.characterInstanceId.instanceId,
diminished = diminished, diminished = diminished,

View file

@ -6,7 +6,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.desktop.lwa.ui.composable.error.ErrorSnackUio 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.desktop.lwa.ui.overlay.roll.RollResult
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
@ -26,7 +26,7 @@ class LevelUpViewModel(
private val levelUpFactory: LevelUpFactory, private val levelUpFactory: LevelUpFactory,
savedStateHandle: SavedStateHandle, savedStateHandle: SavedStateHandle,
) : ViewModel() { ) : ViewModel() {
private val argument = CharacterSheetDestination.Argument(savedStateHandle) private val argument = LevelUpDestination.Argument(savedStateHandle)
private val _errors = MutableSharedFlow<ErrorSnackUio>() private val _errors = MutableSharedFlow<ErrorSnackUio>()
val error: SharedFlow<ErrorSnackUio> = _errors val error: SharedFlow<ErrorSnackUio> = _errors

View file

@ -9,6 +9,7 @@ import androidx.compose.ui.unit.dp
@Stable @Stable
data class LwaShapes( data class LwaShapes(
val portrait: Shape,
val panel: Shape, val panel: Shape,
val settings: Shape, val settings: Shape,
) )
@ -16,10 +17,12 @@ data class LwaShapes(
@Stable @Stable
@Composable @Composable
fun lwaShapes( fun lwaShapes(
panel: Shape = RoundedCornerShape(16.dp), portrait: Shape = RoundedCornerShape(8.dp),
panel: Shape = RoundedCornerShape(8.dp),
settings: Shape = RoundedCornerShape(8.dp), settings: Shape = RoundedCornerShape(8.dp),
): LwaShapes = remember { ): LwaShapes = remember {
LwaShapes( LwaShapes(
portrait = portrait,
panel = panel, panel = panel,
settings = settings, settings = settings,
) )

View file

@ -30,15 +30,15 @@ class CharacterSheetService(
return sheets.map { factory.convertToPreviewJson(sheet = it.value) } return sheets.map { factory.convertToPreviewJson(sheet = it.value) }
} }
fun character(id: String): CharacterSheetJson? { fun characterSheet(id: String): CharacterSheetJson? {
return sheets[id]?.let(factory::convertToJson) return sheets[id]?.let(factory::convertToJson)
} }
suspend fun updateCharacter(character: CharacterSheetJson) { suspend fun updateCharacterSheet(character: CharacterSheetJson) {
return store.save(sheet = factory.convertFromJson(character)) return store.save(sheet = factory.convertFromJson(character))
} }
fun deleteCharacter(characterId: String): Boolean { fun deleteCharacterSheet(characterId: String): Boolean {
return store.delete(id = characterId) return store.delete(id = characterId)
} }

View file

@ -27,6 +27,7 @@ class Engine(
is CampaignMessage -> { is CampaignMessage -> {
val instanceId = Campaign.CharacterInstance.Id( val instanceId = Campaign.CharacterInstance.Id(
prefix = data.prefix,
characterSheetId = data.characterSheetId, characterSheetId = data.characterSheetId,
instanceId = data.instanceId, instanceId = data.instanceId,
) )

View file

@ -4,7 +4,7 @@ package com.pixelized.server.lwa.server
import com.pixelized.server.lwa.server.rest.alteration.getActiveAlteration import com.pixelized.server.lwa.server.rest.alteration.getActiveAlteration
import com.pixelized.server.lwa.server.rest.alteration.getAlteration import com.pixelized.server.lwa.server.rest.alteration.getAlteration
import com.pixelized.server.lwa.server.rest.alteration.putActiveAlteration import com.pixelized.server.lwa.server.rest.alteration.putActiveAlteration
import com.pixelized.server.lwa.server.rest.campaign.deleteCampaignCharacter import com.pixelized.server.lwa.server.rest.campaign.removeCampaignCharacter
import com.pixelized.server.lwa.server.rest.campaign.deleteCampaignNpc import com.pixelized.server.lwa.server.rest.campaign.deleteCampaignNpc
import com.pixelized.server.lwa.server.rest.campaign.getCampaign import com.pixelized.server.lwa.server.rest.campaign.getCampaign
import com.pixelized.server.lwa.server.rest.campaign.putCampaignCharacter import com.pixelized.server.lwa.server.rest.campaign.putCampaignCharacter
@ -142,7 +142,7 @@ class LocalServer {
) )
delete( delete(
path = "/delete", path = "/delete",
body = engine.deleteCampaignCharacter(), body = engine.removeCampaignCharacter(),
) )
} }
route(path = "/npc") { route(path = "/npc") {

View file

@ -1,26 +1,25 @@
package com.pixelized.server.lwa.server.rest.alteration package com.pixelized.server.lwa.server.rest.alteration
import com.pixelized.server.lwa.server.Engine import com.pixelized.server.lwa.server.Engine
import com.pixelized.shared.lwa.model.campaign.Campaign import com.pixelized.server.lwa.utils.extentions.characterInstanceId
import io.ktor.http.HttpStatusCode
import io.ktor.server.response.respond import io.ktor.server.response.respond
import io.ktor.server.response.respondText
fun Engine.getActiveAlteration(): suspend io.ktor.server.routing.RoutingContext.() -> Unit { fun Engine.getActiveAlteration(): suspend io.ktor.server.routing.RoutingContext.() -> Unit {
return { return {
// get the query parameter try {
val characterSheetId = call.queryParameters["characterSheetId"] // get the query parameter
val instanceId = call.queryParameters["instanceId"]?.toIntOrNull() val characterInstanceId = call.queryParameters.characterInstanceId
// build the character instance id. // fetch the data from the service
val id = if (characterSheetId != null && instanceId != null) { val data = alterationService.active(characterInstanceId = characterInstanceId)
Campaign.CharacterInstance.Id( // respond to the client.
characterSheetId = characterSheetId, call.respond(data)
instanceId = instanceId } catch (exception: Exception) {
call.respondText(
text = exception.localizedMessage,
status = HttpStatusCode.UnprocessableEntity,
) )
} else {
null
} }
// fetch the data from the service
val data = id?.let { alterationService.active(it) } ?: emptyList()
// respond to the client.
call.respond(data)
} }
} }

View file

@ -1,7 +1,7 @@
package com.pixelized.server.lwa.server.rest.alteration package com.pixelized.server.lwa.server.rest.alteration
import com.pixelized.server.lwa.server.Engine import com.pixelized.server.lwa.server.Engine
import com.pixelized.shared.lwa.model.campaign.Campaign import com.pixelized.server.lwa.utils.extentions.characterInstanceId
import com.pixelized.shared.lwa.protocol.websocket.Message import com.pixelized.shared.lwa.protocol.websocket.Message
import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
@ -10,54 +10,45 @@ import io.ktor.server.response.respondText
fun Engine.putActiveAlteration(): suspend io.ktor.server.routing.RoutingContext.() -> Unit { fun Engine.putActiveAlteration(): suspend io.ktor.server.routing.RoutingContext.() -> Unit {
return { return {
// fetch the query parameters try {
val characterSheetId = call.queryParameters["characterSheetId"] // get the query parameter
val instanceId = call.queryParameters["instanceId"]?.toIntOrNull() val characterInstanceId = call.queryParameters.characterInstanceId
val alterationId = call.receive<String>()
// build the characterInstanceId from the parameters // fetch the query parameters
val characterInstanceId = if (characterSheetId != null && instanceId != null) { val alterationId = call.receive<String>()
Campaign.CharacterInstance.Id(
characterSheetId = characterSheetId,
instanceId = instanceId
)
} else {
null
}
// Update the alteration // Update the alteration
val updated = characterInstanceId?.let { val updated = alterationService.toggleActiveAlteration(
alterationService.toggleActiveAlteration( characterInstanceId = characterInstanceId,
characterInstanceId = it,
alterationId = alterationId, alterationId = alterationId,
) )
} ?: false if (!updated) {
error("Unexpected error occurred when toggling the alteration (id:$alterationId) for the character (id:$characterInstanceId)")
// build the Http response & send it }
val code = when (updated) { // build the Http response & send it
true -> HttpStatusCode.Accepted call.respondText(
else -> HttpStatusCode.UnprocessableEntity text = "$HttpStatusCode.Accepted",
} status = HttpStatusCode.Accepted,
call.respondText( )
text = "$code", // share the modification to all client through the websocket.
status = code,
)
// share the modification to all client through the websocket.
characterInstanceId?.let {
webSocket.emit( webSocket.emit(
Message( Message(
from = "Server", from = "Server",
value = RestSynchronisation.ToggleActiveAlteration( value = RestSynchronisation.ToggleActiveAlteration(
characterId = campaignJsonFactory.convertToJson(id = it), characterId = campaignJsonFactory.convertToJson(id = characterInstanceId),
alterationId = alterationId, alterationId = alterationId,
active = alterationService.isAlterationActive( active = alterationService.isAlterationActive(
characterInstanceId = it, characterInstanceId = characterInstanceId,
alterationId = alterationId alterationId = alterationId
), ),
), ),
) )
) )
} catch (exception: Exception) {
call.respondText(
text = exception.localizedMessage,
status = HttpStatusCode.UnprocessableEntity,
)
} }
} }
} }

View file

@ -1,38 +1,39 @@
package com.pixelized.server.lwa.server.rest.campaign package com.pixelized.server.lwa.server.rest.campaign
import com.pixelized.server.lwa.server.Engine import com.pixelized.server.lwa.server.Engine
import com.pixelized.shared.lwa.model.campaign.Campaign import com.pixelized.server.lwa.utils.extentions.characterInstanceId
import com.pixelized.shared.lwa.protocol.websocket.Message import com.pixelized.shared.lwa.protocol.websocket.Message
import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.server.response.respondText import io.ktor.server.response.respondText
fun Engine.deleteCampaignCharacter(): suspend io.ktor.server.routing.RoutingContext.() -> Unit { fun Engine.removeCampaignCharacter(): suspend io.ktor.server.routing.RoutingContext.() -> Unit {
return { return {
val characterSheetId = call.queryParameters["characterSheetId"] try {
val instanceId = call.queryParameters["instanceId"]?.toIntOrNull() // get the query parameter
val id = if (characterSheetId != null && instanceId != null) { val characterInstanceId = call.queryParameters.characterInstanceId
Campaign.CharacterInstance.Id( // remove the character form the party
characterSheetId = characterSheetId, val updated = campaignService.removeCharacter(characterInstanceId = characterInstanceId)
instanceId = instanceId // error case
if (!updated) {
error("Unexpected error when removing character (id:$characterInstanceId) from party.")
}
// API & WebSocket responses
call.respondText(
text = "$HttpStatusCode.Accepted",
status = HttpStatusCode.Accepted,
) )
} else { webSocket.emit(
null Message(
} from = "Server",
val updated = id?.let { campaignService.removeCharacter(it) } ?: false value = RestSynchronisation.Campaign,
val code = when (updated) { )
true -> HttpStatusCode.Accepted
else -> HttpStatusCode.UnprocessableEntity
}
call.respondText(
text = "$code",
status = code,
)
webSocket.emit(
Message(
from = "Server",
value = RestSynchronisation.Campaign,
) )
) } catch (exception: Exception) {
call.respondText(
text = exception.localizedMessage,
status = HttpStatusCode.UnprocessableEntity,
)
}
} }
} }

View file

@ -1,7 +1,7 @@
package com.pixelized.server.lwa.server.rest.campaign package com.pixelized.server.lwa.server.rest.campaign
import com.pixelized.server.lwa.server.Engine import com.pixelized.server.lwa.server.Engine
import com.pixelized.shared.lwa.model.campaign.Campaign import com.pixelized.server.lwa.utils.extentions.characterInstanceId
import com.pixelized.shared.lwa.protocol.websocket.Message import com.pixelized.shared.lwa.protocol.websocket.Message
import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
@ -9,30 +9,31 @@ import io.ktor.server.response.respondText
fun Engine.deleteCampaignNpc(): suspend io.ktor.server.routing.RoutingContext.() -> Unit { fun Engine.deleteCampaignNpc(): suspend io.ktor.server.routing.RoutingContext.() -> Unit {
return { return {
val characterSheetId = call.queryParameters["characterSheetId"] try {
val instanceId = call.queryParameters["instanceId"]?.toIntOrNull() // get the query parameter
val id = if (characterSheetId != null && instanceId != null) { val characterInstanceId = call.queryParameters.characterInstanceId
Campaign.CharacterInstance.Id( // remove the character form the party
characterSheetId = characterSheetId, val updated = campaignService.removeNpc(npcInstanceId = characterInstanceId)
instanceId = instanceId // error case
if (!updated) {
error("Unexpected error when removing character (id:$characterInstanceId) from npcs.")
}
// API & WebSocket responses
call.respondText(
text = "$HttpStatusCode.Accepted",
status = HttpStatusCode.Accepted,
) )
} else { webSocket.emit(
null Message(
} from = "Server",
val updated = id?.let { campaignService.removeNpc(it) } ?: false value = RestSynchronisation.Campaign,
val code = when (updated) { )
true -> HttpStatusCode.Accepted
else -> HttpStatusCode.UnprocessableEntity
}
call.respondText(
text = "$code",
status = code,
)
webSocket.emit(
Message(
from = "Server",
value = RestSynchronisation.Campaign,
) )
) } catch (exception: Exception) {
call.respondText(
text = exception.localizedMessage,
status = HttpStatusCode.UnprocessableEntity,
)
}
} }
} }

View file

@ -1,6 +1,7 @@
package com.pixelized.server.lwa.server.rest.campaign package com.pixelized.server.lwa.server.rest.campaign
import com.pixelized.server.lwa.server.Engine import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.characterSheetId
import com.pixelized.shared.lwa.model.campaign.Campaign import com.pixelized.shared.lwa.model.campaign.Campaign
import com.pixelized.shared.lwa.protocol.websocket.Message import com.pixelized.shared.lwa.protocol.websocket.Message
import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation
@ -10,25 +11,26 @@ import io.ktor.server.response.respondText
fun Engine.putCampaignCharacter(): suspend io.ktor.server.routing.RoutingContext.() -> Unit { fun Engine.putCampaignCharacter(): suspend io.ktor.server.routing.RoutingContext.() -> Unit {
return { return {
try { try {
val characterSheetId = call.queryParameters["characterSheetId"] // get the query parameter
?: error("missing character sheet id") val characterSheetId = call.queryParameters.characterSheetId
// check if the character is already in the party.
val instanceId = campaignService.campaign().characters.keys val instanceId = campaignService.campaign().characters.keys
.firstOrNull { key -> key.characterSheetId == characterSheetId } .firstOrNull { key -> key.characterSheetId == characterSheetId }
// handle the error case.
if (instanceId != null) { if (instanceId != null) {
error("Character Already in party") error("Character (characterSheetId:$characterSheetId) Already in party")
} }
// create the instance id for the character.
val id = Campaign.CharacterInstance.Id( val id = Campaign.CharacterInstance.Id(
prefix = Campaign.CharacterInstance.Id.PLAYER,
characterSheetId = characterSheetId, characterSheetId = characterSheetId,
instanceId = 0, instanceId = 0,
) )
// add the character to the party.
if (campaignService.addCharacter(id).not()) { if (campaignService.addCharacter(id).not()) {
error("Unexpected error occurred when the character instance was added to the party") error("Unexpected error occurred when the character instance was added to the party")
} }
// API & WebSocket responses.
call.respondText( call.respondText(
text = "Character $characterSheetId successfully added to the party", text = "Character $characterSheetId successfully added to the party",
status = HttpStatusCode.Accepted, status = HttpStatusCode.Accepted,

View file

@ -1,6 +1,7 @@
package com.pixelized.server.lwa.server.rest.campaign package com.pixelized.server.lwa.server.rest.campaign
import com.pixelized.server.lwa.server.Engine import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.characterSheetId
import com.pixelized.shared.lwa.model.campaign.Campaign import com.pixelized.shared.lwa.model.campaign.Campaign
import com.pixelized.shared.lwa.protocol.websocket.Message import com.pixelized.shared.lwa.protocol.websocket.Message
import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation
@ -10,9 +11,9 @@ import io.ktor.server.response.respondText
fun Engine.putCampaignNpc(): suspend io.ktor.server.routing.RoutingContext.() -> Unit { fun Engine.putCampaignNpc(): suspend io.ktor.server.routing.RoutingContext.() -> Unit {
return { return {
try { try {
val characterSheetId = call.queryParameters["characterSheetId"] // get the query parameter
?: error("missing character sheet id") val characterSheetId = call.queryParameters.characterSheetId
// compute the npc id base on similar character sheets.
val instanceId = campaignService.campaign().npcs.keys val instanceId = campaignService.campaign().npcs.keys
.filter { it.characterSheetId == characterSheetId } .filter { it.characterSheetId == characterSheetId }
.reduceOrNull { acc, id -> .reduceOrNull { acc, id ->
@ -22,16 +23,17 @@ fun Engine.putCampaignNpc(): suspend io.ktor.server.routing.RoutingContext.() ->
acc acc
} }
} }
// create the instance id for the character.
val id = Campaign.CharacterInstance.Id( val id = Campaign.CharacterInstance.Id(
prefix = Campaign.CharacterInstance.Id.NPC,
characterSheetId = characterSheetId, characterSheetId = characterSheetId,
instanceId = instanceId?.let { it.instanceId + 1 } ?: 0, instanceId = instanceId?.let { it.instanceId + 1 } ?: 0,
) )
// add the character to the npcs.
if (campaignService.addNpc(id).not()) { if (campaignService.addNpc(id).not()) {
error("Unexpected error occurred when the character instance was added to the npcs") error("Unexpected error occurred when the character instance was added to the npcs")
} }
// API & WebSocket responses.
call.respondText( call.respondText(
text = "Character $characterSheetId successfully added to the npcs", text = "Character $characterSheetId successfully added to the npcs",
status = HttpStatusCode.Accepted, status = HttpStatusCode.Accepted,

View file

@ -1,6 +1,7 @@
package com.pixelized.server.lwa.server.rest.character package com.pixelized.server.lwa.server.rest.character
import com.pixelized.server.lwa.server.Engine import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.characterSheetId
import com.pixelized.shared.lwa.protocol.websocket.Message import com.pixelized.shared.lwa.protocol.websocket.Message
import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
@ -8,10 +9,10 @@ import io.ktor.server.response.respondText
fun Engine.deleteCharacter(): suspend io.ktor.server.routing.RoutingContext.() -> Unit { fun Engine.deleteCharacter(): suspend io.ktor.server.routing.RoutingContext.() -> Unit {
return { return {
val id = call.parameters["id"] val characterSheetId = call.parameters.characterSheetId
val deleted = id?.let(characterService::deleteCharacter) ?: false val deleted = characterService.deleteCharacterSheet(characterId = characterSheetId)
if (deleted && id != null) { if (deleted) {
call.respondText( call.respondText(
text = "${HttpStatusCode.OK}", text = "${HttpStatusCode.OK}",
status = HttpStatusCode.OK, status = HttpStatusCode.OK,
@ -19,7 +20,7 @@ fun Engine.deleteCharacter(): suspend io.ktor.server.routing.RoutingContext.() -
webSocket.emit( webSocket.emit(
Message( Message(
from = "Server", from = "Server",
value = RestSynchronisation.CharacterDelete(characterId = id), value = RestSynchronisation.CharacterDelete(characterId = characterSheetId),
) )
) )
} else { } else {

View file

@ -1,15 +1,16 @@
package com.pixelized.server.lwa.server.rest.character package com.pixelized.server.lwa.server.rest.character
import com.pixelized.server.lwa.server.Engine import com.pixelized.server.lwa.server.Engine
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJson import com.pixelized.server.lwa.utils.extentions.characterSheetId
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.server.response.respond import io.ktor.server.response.respond
import io.ktor.server.response.respondText import io.ktor.server.response.respondText
fun Engine.getCharacter(): suspend io.ktor.server.routing.RoutingContext.() -> Unit { fun Engine.getCharacter(): suspend io.ktor.server.routing.RoutingContext.() -> Unit {
return { return {
val id = call.queryParameters["id"] val id = call.queryParameters.characterSheetId
val body: CharacterSheetJson? = id?.let(characterService::character) val body = characterService.characterSheet(id)
if (body != null) { if (body != null) {
call.respond(body) call.respond(body)
} else { } else {

View file

@ -11,7 +11,7 @@ import io.ktor.server.response.respondText
fun Engine.putCharacter(): suspend io.ktor.server.routing.RoutingContext.() -> Unit { fun Engine.putCharacter(): suspend io.ktor.server.routing.RoutingContext.() -> Unit {
return { return {
val form = call.receive<CharacterSheetJson>() val form = call.receive<CharacterSheetJson>()
characterService.updateCharacter( characterService.updateCharacterSheet(
character = form character = form
) )
call.respondText( call.respondText(

View file

@ -0,0 +1,20 @@
package com.pixelized.server.lwa.utils.extentions
import com.pixelized.shared.lwa.model.campaign.Campaign
import io.ktor.http.Parameters
val Parameters.characterInstanceId: Campaign.CharacterInstance.Id
get() = Campaign.CharacterInstance.Id(
characterSheetId = characterSheetId,
instanceId = instanceId,
prefix = prefix,
)
val Parameters.characterSheetId
get() = this["characterSheetId"] ?: error("Missing character sheet id.")
val Parameters.instanceId: Int
get() = this["instanceId"]?.toIntOrNull() ?: error("Missing character instance id.")
val Parameters.prefix: Char
get() = this["prefix"]?.get(0) ?: error("Missing character prefix.")

View file

@ -10,9 +10,25 @@ data class Campaign(
val diminished: Int, val diminished: Int,
) { ) {
data class Id( data class Id(
val prefix: Char,
val characterSheetId: String, val characterSheetId: String,
val instanceId: Int, val instanceId: Int,
) ) {
fun equals(
prefix: Char,
characterSheetId: String?,
instanceId: Int?,
): Boolean {
return this.prefix == prefix &&
this.characterSheetId == characterSheetId &&
this.instanceId == instanceId
}
companion object {
const val PLAYER = 'c'
const val NPC = 'n'
}
}
enum class Characteristic { enum class Characteristic {
Damage, Damage,

View file

@ -65,7 +65,7 @@ class CampaignJsonFactory(
fun convertToJson( fun convertToJson(
id: Campaign.CharacterInstance.Id, id: Campaign.CharacterInstance.Id,
): String { ): String {
return "${String.format("%03d", id.instanceId)}-${id.characterSheetId}" return "${id.prefix}-${String.format("%03d", id.instanceId)}-${id.characterSheetId}"
} }
fun convertToJson( fun convertToJson(

View file

@ -33,8 +33,9 @@ class CampaignJsonV1Factory {
characterInstanceIdJson: String, characterInstanceIdJson: String,
): Campaign.CharacterInstance.Id { ): Campaign.CharacterInstance.Id {
return Campaign.CharacterInstance.Id( return Campaign.CharacterInstance.Id(
characterSheetId = characterInstanceIdJson.drop(4), // drop first 3 number then the - prefix = characterInstanceIdJson.take(1)[0],
instanceId = characterInstanceIdJson.take(3).toIntOrNull() ?: 0, characterSheetId = characterInstanceIdJson.drop(2 + 4), // drop the char then the - then the first 3 number then the -
instanceId = characterInstanceIdJson.drop(2).take(3).toIntOrNull() ?: 0,
) )
} }

View file

@ -5,11 +5,13 @@ import kotlinx.serialization.Serializable
@Serializable @Serializable
sealed interface CampaignMessage : MessagePayload { sealed interface CampaignMessage : MessagePayload {
val prefix: Char
val characterSheetId: String val characterSheetId: String
val instanceId: Int val instanceId: Int
@Serializable @Serializable
data class UpdateCharacteristic( data class UpdateCharacteristic(
override val prefix: Char,
override val characterSheetId: String, override val characterSheetId: String,
override val instanceId: Int, override val instanceId: Int,
val characteristic: CampaignJsonV1.CharacterInstanceJsonV1.CharacteristicV1, val characteristic: CampaignJsonV1.CharacterInstanceJsonV1.CharacteristicV1,
@ -18,6 +20,7 @@ sealed interface CampaignMessage : MessagePayload {
@Serializable @Serializable
data class UpdateDiminished( data class UpdateDiminished(
override val prefix: Char,
override val characterSheetId: String, override val characterSheetId: String,
override val instanceId: Int, override val instanceId: Int,
val diminished: Int, val diminished: Int,

View file

@ -6,7 +6,9 @@ import java.util.UUID
@Serializable @Serializable
data class RollMessage( data class RollMessage(
val id: RollId, val id: RollId,
val prefix: Char,
val characterSheetId: String, val characterSheetId: String,
val instanceId: Int?,
val skillLabel: String, val skillLabel: String,
val rollValue: Int, val rollValue: Int,
val resultLabel: String? = null, val resultLabel: String? = null,