From 16b2b49f0324fbe7cbbc204d62c5be701877cb08 Mon Sep 17 00:00:00 2001 From: Thomas Andres Gomez Date: Fri, 7 Mar 2025 15:49:36 +0100 Subject: [PATCH] LevelUp feature. --- .../drawable/ic_cancel_24dp.xml | 9 + .../composeResources/values/strings.xml | 11 + .../kotlin/com/pixelized/desktop/lwa/App.kt | 42 +- .../com/pixelized/desktop/lwa/Module.kt | 6 +- .../desktop/lwa/network/LwaClientImpl.kt | 16 +- .../alteration/AlterationRepository.kt | 4 +- .../CharacterSheetRepository.kt | 21 +- .../characterSheet/CharacterSheetStore.kt | 6 +- .../repository/network/NetworkRepository.kt | 16 +- .../lwa/ui/composable/error/ErrorSnackUio.kt | 14 +- .../lwa/ui/composable/shapes/LevelShape.kt | 46 +++ .../{circle => shapes}/MasteryShape.kt | 2 +- .../lwa/ui/navigation/screen/MainNavHost.kt | 7 +- .../destination/CharacterSheetDestination.kt | 10 +- .../screen/destination/LevelUpDestination.kt | 61 +++ .../screen/destination/MainDestination.kt | 4 +- .../screen/destination/NetworkDestination.kt | 3 + .../screen/destination/OldMainDestination.kt | 3 + .../lwa/ui/overlay/roll/RollHostState.kt | 75 ++++ .../lwa/ui/overlay/roll/RollOverlay.kt | 87 ++++ .../ui/{screen => overlay}/roll/RollPage.kt | 108 +++-- .../lwa/ui/overlay/roll/RollViewModel.kt | 301 ++++++++++++++ .../lwa/ui/screen/campaign/CampaignScreen.kt | 51 +-- .../campaign/chat/TextMessageFactory.kt | 13 +- .../campaign/player/detail/CharacterDetail.kt | 30 +- .../player/detail/CharacterDetailFactory.kt | 75 +++- .../player/detail/CharacterDetailViewModel.kt | 8 +- .../detail/header/CharacterDetailHeader.kt | 314 ++++++++------ .../detail/sheet/CharacterDetailSheet.kt | 3 +- .../sheet/CharacterDetailSheetAction.kt | 5 +- .../CharacterDetailSheetCharacteristic.kt | 2 +- .../detail/sheet/CharacterDetailSheetSkill.kt | 7 +- .../campaign/player/ribbon/PlayerPortrait.kt | 13 + .../campaign/player/ribbon/PlayerRibbon.kt | 2 + .../player/ribbon/PlayerRibbonFactory.kt | 1 + .../player/ribbon/PlayerRibbonViewModel.kt | 8 +- .../campaign/toolbar/CampaignToolbar.kt | 2 +- .../detail/CharacterSheetFactory.kt | 83 ++-- .../detail/CharacterSheetPage.kt | 35 +- .../detail/CharacterSheetViewModel.kt | 8 +- .../edit/CharacterSheetEditFactory.kt | 1 + .../lwa/ui/screen/levelup/LevelScreen.kt | 384 ++++++++++++++++++ .../lwa/ui/screen/levelup/LevelUpFactory.kt | 225 ++++++++++ .../lwa/ui/screen/levelup/LevelUpViewModel.kt | 157 +++++++ .../levelup/skill/LevelUpCharacteristic.kt | 93 +++++ .../ui/screen/levelup/skill/LevelUpSkill.kt | 124 ++++++ .../lwa/ui/screen/main/MainPageViewModel.kt | 4 +- .../lwa/ui/screen/network/NetworkFactory.kt | 6 +- .../lwa/ui/screen/network/NetworkScreen.kt | 75 +++- .../lwa/ui/screen/network/NetworkViewModel.kt | 49 ++- .../lwa/ui/screen/roll/RollActionUio.kt | 11 - .../lwa/ui/screen/roll/RollViewModel.kt | 253 ------------ .../rollhistory/RollHistoryViewModel.kt | 2 +- .../lwa/ui/screen/settings/SettingsScreen.kt | 12 +- .../ui/screen/settings/SettingsViewModel.kt | 10 +- .../desktop/lwa/ui/theme/LwaTheme.kt | 5 + .../desktop/lwa/ui/theme/color/LwaColors.kt | 10 + .../desktop/lwa/ui/theme/shapes/ArrowShape.kt | 83 ++++ .../desktop/lwa/ui/theme/shapes/LwaShapes.kt | 3 + .../desktop/lwa/ui/theme/size/LwaSize.kt | 22 + .../desktop/lwa/utils/extention/JsonExt.kt | 16 - .../lwa/business/DamageBonusUseCaseTest.kt | 12 +- .../parser/expression/ExpressionParserTest.kt | 54 ++- .../pixelized/server/lwa/extention/JsonExt.kt | 17 - .../lwa/model/campaign/CampaignService.kt | 1 - .../com/pixelized/server/lwa/server/Server.kt | 18 +- .../shared/lwa/model/AlteredCharacterSheet.kt | 7 +- .../model/characterSheet/CharacterSheet.kt | 1 + .../CharacterSheetJsonFactory.kt | 2 + .../characterSheet/CharacterSheetJsonV1.kt | 1 + .../lwa/parser/expression/Expression.kt | 8 + .../lwa/parser/expression/ExpressionParser.kt | 19 + .../protocol/websocket/payload/RollMessage.kt | 25 +- .../lwa/usecase/CharacterSheetUseCase.kt | 26 +- .../shared/lwa/usecase/ExpressionUseCase.kt | 61 ++- 75 files changed, 2532 insertions(+), 777 deletions(-) create mode 100644 composeApp/src/commonMain/composeResources/drawable/ic_cancel_24dp.xml create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/shapes/LevelShape.kt rename composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/{circle => shapes}/MasteryShape.kt (97%) create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/LevelUpDestination.kt create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/overlay/roll/RollHostState.kt create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/overlay/roll/RollOverlay.kt rename composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/{screen => overlay}/roll/RollPage.kt (83%) create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/overlay/roll/RollViewModel.kt create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/levelup/LevelScreen.kt create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/levelup/LevelUpFactory.kt create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/levelup/LevelUpViewModel.kt create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/levelup/skill/LevelUpCharacteristic.kt create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/levelup/skill/LevelUpSkill.kt delete mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/roll/RollActionUio.kt delete mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/roll/RollViewModel.kt create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/shapes/ArrowShape.kt create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/size/LwaSize.kt delete mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/utils/extention/JsonExt.kt delete mode 100644 server/src/main/kotlin/com/pixelized/server/lwa/extention/JsonExt.kt diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_cancel_24dp.xml b/composeApp/src/commonMain/composeResources/drawable/ic_cancel_24dp.xml new file mode 100644 index 0000000..282a9a5 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/ic_cancel_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml index 352544a..533e975 100644 --- a/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -1,5 +1,9 @@ + + La feuille de personnage est introuvable + Ok + La table de Lwa Confirmer @@ -82,6 +86,7 @@ Supprimer Compétence d'occupation + niv : %1$d État diminué Modifier Supprimer @@ -153,6 +158,7 @@ Aucun Vous êtes connecté au serveur Vous êtes déconnecté du serveur + Ok Historique des lancers lance @@ -178,4 +184,9 @@ Défilement automatique Défilement automatique du chat vers le dernier message reçu lors de la réception de ce dernier. + Montée de niveau + Level Up ! + Passage du niveau %1$d au niveau supérieur : %2$d + niv : %1$d - + \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/App.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/App.kt index 7b34129..1fa5ce5 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/App.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/App.kt @@ -37,6 +37,9 @@ import coil3.compose.setSingletonImageLoaderFactory import coil3.request.crossfade import com.pixelized.desktop.lwa.repository.network.NetworkRepository import com.pixelized.desktop.lwa.repository.network.NetworkRepository.Status +import com.pixelized.desktop.lwa.ui.composable.blur.BlurContent +import com.pixelized.desktop.lwa.ui.composable.blur.BlurContentController +import com.pixelized.desktop.lwa.ui.composable.blur.rememberBlurContentController import com.pixelized.desktop.lwa.ui.composable.key.KeyEventHandler import com.pixelized.desktop.lwa.ui.composable.key.LocalKeyEventHandlers import com.pixelized.desktop.lwa.ui.navigation.screen.MainNavHost @@ -56,6 +59,8 @@ import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.PlayerRibbon import com.pixelized.desktop.lwa.ui.screen.characterSheet.CharacterSheetMainNavHost import com.pixelized.desktop.lwa.ui.screen.network.NetworkPage import com.pixelized.desktop.lwa.ui.screen.network.NetworkViewModel +import com.pixelized.desktop.lwa.ui.overlay.roll.RollHostState +import com.pixelized.desktop.lwa.ui.overlay.roll.RollOverlay import com.pixelized.desktop.lwa.ui.screen.rollhistory.RollHistoryPage import com.pixelized.desktop.lwa.ui.screen.rollhistory.RollHistoryViewModel import com.pixelized.desktop.lwa.ui.theme.LwaTheme @@ -65,6 +70,7 @@ import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.app_name import lwacharactersheet.composeapp.generated.resources.network__connect__message import lwacharactersheet.composeapp.generated.resources.network__disconnect__message +import lwacharactersheet.composeapp.generated.resources.network__message__action import org.jetbrains.compose.resources.getString import org.jetbrains.compose.ui.tooling.preview.Preview import org.koin.compose.koinInject @@ -86,6 +92,14 @@ val LocalApplicationScope = compositionLocalOf { error("Local application scope is not yet ready") } +val LocalRollHostState = compositionLocalOf { + error("Local Roll Host State is not yet ready") +} + +val LocalBlurController = compositionLocalOf { + error("Local Blur Controller is not yet ready") +} + @Composable @Preview fun ApplicationScope.App() { @@ -94,6 +108,8 @@ fun ApplicationScope.App() { val errorSnackHostState = remember { SnackbarHostState() } val windowController = remember { WindowController(maxWindowHeight) } val keyEventHandlers = remember { mutableStateListOf() } + val rollHostState = remember { RollHostState() } + val blurController = rememberBlurContentController() val windowsState = rememberWindowState( size = DpSize( width = 800.dp, @@ -117,6 +133,8 @@ fun ApplicationScope.App() { LocalErrorSnackHost provides errorSnackHostState, LocalWindowController provides windowController, LocalKeyEventHandlers provides keyEventHandlers, + LocalRollHostState provides rollHostState, + LocalBlurController provides blurController, LocalWindowState provides windowsState, ) { Window( @@ -147,6 +165,8 @@ private fun MainWindowScreen( val snackHostState = LocalSnackHost.current val errorSnackHostState = LocalErrorSnackHost.current val windowController = LocalWindowController.current + val rollHostState = LocalRollHostState.current + val blurController = LocalBlurController.current LwaTheme { Surface( @@ -163,6 +183,9 @@ private fun MainWindowScreen( snackbar = { Snackbar( snackbarData = it, + backgroundColor = MaterialTheme.colors.surface, + contentColor = MaterialTheme.colors.onSurface, + actionColor = MaterialTheme.colors.onSurface, ) } ) @@ -180,10 +203,18 @@ private fun MainWindowScreen( } }, content = { - MainNavHost( - campaignViewModel = campaignViewModel, - networkViewModel = networkViewModel, - campaignChatViewModel = campaignChatViewModel, + BlurContent( + modifier = Modifier.fillMaxSize(), + controller = blurController + ) { + MainNavHost( + campaignViewModel = campaignViewModel, + networkViewModel = networkViewModel, + campaignChatViewModel = campaignChatViewModel, + ) + } + RollOverlay( + hostState = rollHostState, ) } ) @@ -209,7 +240,7 @@ private fun WindowsHandler( when (window) { is CharacterSheetWindow -> CharacterSheetMainNavHost( startDestination = CharacterSheetDestination.navigationRoute( - id = window.characterId, + characterInstanceId = window.characterId, ), ) @@ -247,6 +278,7 @@ private fun NetworkSnackHandler( } snack.showSnackbar( message = message, + actionLabel = getString(Res.string.network__message__action), duration = SnackbarDuration.Short, ) } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/Module.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/Module.kt index 23e5558..c4b528d 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/Module.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/Module.kt @@ -16,6 +16,7 @@ import com.pixelized.desktop.lwa.repository.settings.SettingsStore import com.pixelized.desktop.lwa.ui.composable.character.characteristic.CharacterDetailCharacteristicDialogViewModel import com.pixelized.desktop.lwa.ui.composable.character.characteristic.CharacterSheetCharacteristicDialogFactory import com.pixelized.desktop.lwa.ui.screen.campaign.CampaignViewModel +import com.pixelized.desktop.lwa.ui.screen.levelup.LevelUpViewModel import com.pixelized.desktop.lwa.ui.screen.campaign.chat.CampaignChatViewModel import com.pixelized.desktop.lwa.ui.screen.campaign.chat.TextMessageFactory import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailFactory @@ -28,10 +29,11 @@ import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.CharacterSheetV import com.pixelized.desktop.lwa.ui.screen.characterSheet.edit.CharacterSheetEditFactory import com.pixelized.desktop.lwa.ui.screen.characterSheet.edit.CharacterSheetEditViewModel import com.pixelized.desktop.lwa.ui.screen.characterSheet.edit.common.SkillFieldFactory +import com.pixelized.desktop.lwa.ui.screen.levelup.LevelUpFactory import com.pixelized.desktop.lwa.ui.screen.main.MainPageViewModel import com.pixelized.desktop.lwa.ui.screen.network.NetworkFactory import com.pixelized.desktop.lwa.ui.screen.network.NetworkViewModel -import com.pixelized.desktop.lwa.ui.screen.roll.RollViewModel +import com.pixelized.desktop.lwa.ui.overlay.roll.RollViewModel import com.pixelized.desktop.lwa.ui.screen.rollhistory.RollHistoryViewModel import com.pixelized.desktop.lwa.ui.screen.settings.SettingsViewModel import com.pixelized.desktop.lwa.usecase.SettingsUseCase @@ -109,6 +111,7 @@ val factoryDependencies factoryOf(::CharacterDetailFactory) factoryOf(::CharacterSheetCharacteristicDialogFactory) factoryOf(::TextMessageFactory) + factoryOf(::LevelUpFactory) } val viewModelDependencies @@ -126,6 +129,7 @@ val viewModelDependencies viewModelOf(::CharacterDetailCharacteristicDialogViewModel) viewModelOf(::CampaignChatViewModel) viewModelOf(::SettingsViewModel) + viewModelOf(::LevelUpViewModel) } val useCaseDependencies diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClientImpl.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClientImpl.kt index 8b0d08d..bb0373c 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClientImpl.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClientImpl.kt @@ -13,7 +13,6 @@ import io.ktor.client.request.put import io.ktor.client.request.setBody import io.ktor.http.ContentType import io.ktor.http.contentType -import io.ktor.http.path class LwaClientImpl( private val client: HttpClient, @@ -30,12 +29,9 @@ class LwaClientImpl( .body() override suspend fun updateCharacter(sheet: CharacterSheetJson) = client - .put { - url { - path("$host/character/update?id=") - contentType(ContentType.Application.Json) - setBody(sheet) - } + .put("$root/character/update") { + contentType(ContentType.Application.Json) + setBody(sheet) } .body() @@ -92,10 +88,8 @@ class LwaClientImpl( alterationId: String, ) = client .put("$root/alterations/active/toggle?characterSheetId=$characterSheetId&instanceId=$instanceId") { - url { - contentType(ContentType.Application.Json) - setBody(alterationId) - } + contentType(ContentType.Application.Json) + setBody(alterationId) } .body() } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/alteration/AlterationRepository.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/alteration/AlterationRepository.kt index 84336ed..26e685e 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/alteration/AlterationRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/alteration/AlterationRepository.kt @@ -38,9 +38,9 @@ class AlterationRepository( ) fun alterationsFlow( - characterId: CharacterInstance.Id, + characterInstanceId: CharacterInstance.Id, ): Flow>> { - return activeAlterationMapFlow.map { it[characterId] ?: emptyMap() } + return activeAlterationMapFlow.map { it[characterInstanceId] ?: emptyMap() } } fun alterations( diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetRepository.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetRepository.kt index 0476594..0cdb070 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetRepository.kt @@ -27,19 +27,24 @@ class CharacterSheetRepository( forceUpdate: Boolean = false, ): CharacterSheet? { return try { - characterSheetId?.let { store.characterDetail(characterId = it, forceUpdate = forceUpdate) } + characterSheetId?.let { + store.characterDetail( + characterId = it, + forceUpdate = forceUpdate + ) + } } catch (exception: Exception) { null } } fun characterDetailFlow( - characterId: String?, + characterSheetId: String?, ): StateFlow { - val initial = store.detailFlow.value[characterId] + val initial = store.detailFlow.value[characterSheetId] return store.detailFlow .map { sheets -> - sheets[characterId] + sheets[characterSheetId] } .stateIn( scope = scope, @@ -48,11 +53,15 @@ class CharacterSheetRepository( ) } - suspend fun updateCharacter(characterSheet: CharacterSheet) { + suspend fun updateCharacter( + characterSheet: CharacterSheet, + ) { store.updateCharacter(sheet = characterSheet) } - suspend fun deleteCharacter(characterId: String) { + suspend fun deleteCharacter( + characterId: String, + ) { store.deleteCharacter(characterId = characterId) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetStore.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetStore.kt index 7a9c602..a5eba7d 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetStore.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetStore.kt @@ -24,6 +24,7 @@ class CharacterSheetStore( ) { private val _previewFlow = MutableStateFlow>(value = emptyList()) val previewFlow: StateFlow> get() = _previewFlow + private val _detailFlow = MutableStateFlow>(value = emptyMap()) val detailFlow: StateFlow> get() = _detailFlow @@ -75,7 +76,8 @@ class CharacterSheetStore( try { client.updateCharacter(sheet = json) } catch (exception: Exception) { - + // TODO + println(exception) } _detailFlow.update(sheet = sheet) } @@ -87,7 +89,7 @@ class CharacterSheetStore( client.deleteCharacter(id = characterId) _detailFlow.delete(characterId = characterId) } catch (exception: Exception) { - + // TODO } } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/network/NetworkRepository.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/network/NetworkRepository.kt index eb56eff..2ce2d1e 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/network/NetworkRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/network/NetworkRepository.kt @@ -2,8 +2,6 @@ package com.pixelized.desktop.lwa.repository.network import com.pixelized.desktop.lwa.repository.network.helper.connectWebSocket import com.pixelized.desktop.lwa.repository.settings.SettingsRepository -import com.pixelized.desktop.lwa.utils.extention.decodeFromFrame -import com.pixelized.desktop.lwa.utils.extention.encodeToFrame import com.pixelized.shared.lwa.protocol.websocket.Message import com.pixelized.shared.lwa.protocol.websocket.payload.MessagePayload import io.ktor.client.HttpClient @@ -12,6 +10,7 @@ import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.plugins.websocket.WebSockets import io.ktor.serialization.kotlinx.json.json import io.ktor.websocket.Frame +import io.ktor.websocket.readText import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -47,6 +46,7 @@ class NetworkRepository( onClose: () -> Unit = { }, ) { client = buildNewClient() + networkJob?.cancel() networkJob = scope.launch(Dispatchers.IO) { try { @@ -57,17 +57,23 @@ class NetworkRepository( val job = launch { // send message to the server outgoingMessageBuffer.collect { message -> - send(Json.encodeToFrame(message = message)) + val data = json.encodeToString(message) + val frame = Frame.Text(text = data) + send(frame = frame) } } - runBlocking { + runCatching { // watching for server incoming message incoming.consumeEach { frame -> if (frame is Frame.Text) { - val message = Json.decodeFromFrame(frame = frame) + val data = frame.readText() + val message = json.decodeFromString(data) incomingMessageBuffer.emit(message) } } + }.onFailure { exception -> + // TODO + println("WebSocket exception: ${exception.localizedMessage}") }.also { job.cancel() } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/error/ErrorSnackUio.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/error/ErrorSnackUio.kt index 857e1da..b45f07f 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/error/ErrorSnackUio.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/error/ErrorSnackUio.kt @@ -11,14 +11,16 @@ import kotlinx.coroutines.flow.SharedFlow @Stable class ErrorSnackUio( val message: String, - val action: String?, + val action: String, val duration: SnackbarDuration, ) { - constructor(exception: Exception) : this( - message = exception.localizedMessage, - action = "Ok", - duration = SnackbarDuration.Indefinite, - ) + companion object { + fun from(exception: Exception) = ErrorSnackUio( + message = exception.localizedMessage, + action = "Ok", + duration = SnackbarDuration.Indefinite + ) + } } @Composable diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/shapes/LevelShape.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/shapes/LevelShape.kt new file mode 100644 index 0000000..f6bea20 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/shapes/LevelShape.kt @@ -0,0 +1,46 @@ +package com.pixelized.desktop.lwa.ui.composable.shapes + +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.unit.dp +import com.pixelized.desktop.lwa.ui.theme.lwa +import com.pixelized.desktop.lwa.ui.theme.shapes.ArrowShape + +@Composable +fun ArrowShape() { + val colorScheme = MaterialTheme.lwa.colorScheme + val arrow = remember { + ArrowShape( + core = 4.dp, + head = 4.dp, + ) + } + Box( + modifier = Modifier + .size(size = 24.dp) + .padding(horizontal = 2.dp) + .padding(top = 3.dp, bottom = 3.dp) + .border( + width = 1.dp, + color = colorScheme.portrait.levelUp, + shape = arrow, + ) + .shadow( + elevation = 1.dp, + shape = arrow, + ) + .shadow( + elevation = 2.dp, + shape = arrow, + ambientColor = colorScheme.portrait.levelUp, + spotColor = colorScheme.portrait.levelUp, + ) + ) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/circle/MasteryShape.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/shapes/MasteryShape.kt similarity index 97% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/circle/MasteryShape.kt rename to composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/shapes/MasteryShape.kt index c743ae6..ce72575 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/circle/MasteryShape.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/shapes/MasteryShape.kt @@ -1,4 +1,4 @@ -package com.pixelized.desktop.lwa.ui.composable.circle +package com.pixelized.desktop.lwa.ui.composable.shapes import androidx.compose.foundation.background import androidx.compose.foundation.border diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/MainNavHost.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/MainNavHost.kt index 2fd1f09..46d8789 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/MainNavHost.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/MainNavHost.kt @@ -7,6 +7,7 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.rememberNavController import com.pixelized.desktop.lwa.ui.navigation.screen.destination.MainDestination +import com.pixelized.desktop.lwa.ui.navigation.screen.destination.composableLevelUp import com.pixelized.desktop.lwa.ui.navigation.screen.destination.composableMainPage import com.pixelized.desktop.lwa.ui.navigation.screen.destination.composableNetworkPage import com.pixelized.desktop.lwa.ui.navigation.screen.destination.composableOldMainPage @@ -39,9 +40,11 @@ fun MainNavHost( networkViewModel = networkViewModel, campaignChatViewModel = campaignChatViewModel, ) - composableOldMainPage() - composableNetworkPage() composableSettingsPage() + composableLevelUp() + + composableNetworkPage() + composableOldMainPage() } } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/CharacterSheetDestination.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/CharacterSheetDestination.kt index ef5442a..0c0d785 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/CharacterSheetDestination.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/CharacterSheetDestination.kt @@ -17,9 +17,9 @@ object CharacterSheetDestination { fun baseRoute() = "$ROUTE?${CHARACTER_SHEET_ID.ARG}&${CHARACTER_INSTANCE_ID.ARG}" - fun navigationRoute(id: Campaign.CharacterInstance.Id) = ROUTE + - "?$CHARACTER_SHEET_ID=${id.characterSheetId}" + - "&$CHARACTER_INSTANCE_ID=${id.instanceId}" + fun navigationRoute(characterInstanceId: Campaign.CharacterInstance.Id) = ROUTE + + "?$CHARACTER_SHEET_ID=${characterInstanceId.characterSheetId}" + + "&$CHARACTER_INSTANCE_ID=${characterInstanceId.instanceId}" fun arguments() = listOf( navArgument(CHARACTER_SHEET_ID) { @@ -54,8 +54,8 @@ fun NavGraphBuilder.composableCharacterSheetPage() { } fun NavHostController.navigateToCharacterSheet( - id: Campaign.CharacterInstance.Id, + characterInstanceId: Campaign.CharacterInstance.Id, ) { - val route = CharacterSheetDestination.navigationRoute(id = id) + val route = CharacterSheetDestination.navigationRoute(characterInstanceId = characterInstanceId) navigate(route = route) } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/LevelUpDestination.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/LevelUpDestination.kt new file mode 100644 index 0000000..9d24eec --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/LevelUpDestination.kt @@ -0,0 +1,61 @@ +package com.pixelized.desktop.lwa.ui.navigation.screen.destination + +import androidx.lifecycle.SavedStateHandle +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavHostController +import androidx.navigation.NavType +import androidx.navigation.compose.composable +import androidx.navigation.navArgument +import com.pixelized.desktop.lwa.ui.screen.levelup.LevelUpScreen +import com.pixelized.desktop.lwa.utils.extention.ARG +import com.pixelized.shared.lwa.model.campaign.Campaign + +object LevelUpDestination { + private const val ROUTE = "levelUp" + private const val CHARACTER_SHEET_ID = "sheetId" + private const val CHARACTER_INSTANCE_ID = "instanceId" + + fun baseRoute() = "${ROUTE}?${CHARACTER_SHEET_ID.ARG}&${CHARACTER_INSTANCE_ID.ARG}" + + fun navigationRoute(characterInstanceId: Campaign.CharacterInstance.Id) = ROUTE + + "?$CHARACTER_SHEET_ID=${characterInstanceId.characterSheetId}" + + "&$CHARACTER_INSTANCE_ID=${characterInstanceId.instanceId}" + + fun arguments() = listOf( + navArgument(CHARACTER_SHEET_ID) { + nullable = false + type = NavType.StringType + }, + navArgument(CHARACTER_INSTANCE_ID) { + nullable = false + type = NavType.IntType + }, + ) + + data class Argument( + val characterInstanceId: Campaign.CharacterInstance.Id, + ) { + constructor(savedStateHandle: SavedStateHandle) : this( + characterInstanceId = Campaign.CharacterInstance.Id( + savedStateHandle.get(CHARACTER_SHEET_ID) ?: error("missing character id"), + savedStateHandle.get(CHARACTER_INSTANCE_ID) ?: error("missing character id"), + ), + ) + } +} + +fun NavGraphBuilder.composableLevelUp() { + composable( + route = LevelUpDestination.baseRoute(), + arguments = LevelUpDestination.arguments(), + ) { + LevelUpScreen() + } +} + +fun NavHostController.navigateToLevelScreen( + characterInstanceId: Campaign.CharacterInstance.Id, +) { + val route = LevelUpDestination.navigationRoute(characterInstanceId = characterInstanceId) + navigate(route = route) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/MainDestination.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/MainDestination.kt index f6d5817..2b7f3f8 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/MainDestination.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/MainDestination.kt @@ -4,7 +4,7 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavHostController import androidx.navigation.compose.composable import com.pixelized.desktop.lwa.ui.screen.campaign.CampaignViewModel -import com.pixelized.desktop.lwa.ui.screen.campaign.MainPage +import com.pixelized.desktop.lwa.ui.screen.campaign.CampaignScreen import com.pixelized.desktop.lwa.ui.screen.campaign.chat.CampaignChatViewModel import com.pixelized.desktop.lwa.ui.screen.network.NetworkViewModel @@ -23,7 +23,7 @@ fun NavGraphBuilder.composableMainPage( composable( route = MainDestination.baseRoute(), ) { - MainPage( + CampaignScreen( campaignViewModel = campaignViewModel, networkViewModel = networkViewModel, campaignChatViewModel = campaignChatViewModel, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/NetworkDestination.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/NetworkDestination.kt index 84cc322..15b53a3 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/NetworkDestination.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/NetworkDestination.kt @@ -5,6 +5,7 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.composable import com.pixelized.desktop.lwa.ui.screen.network.NetworkScreen +@Deprecated(message = "Part of the old UI") object NetworkDestination { private const val ROUTE = "network" @@ -12,6 +13,7 @@ object NetworkDestination { fun navigationRoute() = ROUTE } +@Deprecated(message = "Part of the old UI") fun NavGraphBuilder.composableNetworkPage() { composable( route = NetworkDestination.baseRoute(), @@ -20,6 +22,7 @@ fun NavGraphBuilder.composableNetworkPage() { } } +@Deprecated(message = "Part of the old UI") fun NavHostController.navigateToNetwork() { val route = NetworkDestination.navigationRoute() navigate(route = route) diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/OldMainDestination.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/OldMainDestination.kt index 307eb2e..f572b43 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/OldMainDestination.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/OldMainDestination.kt @@ -5,6 +5,7 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.composable import com.pixelized.desktop.lwa.ui.screen.main.OldMainPage +@Deprecated(message = "Part of the old UI") object OldMainDestination { private const val ROUTE = "old_main" @@ -12,6 +13,7 @@ object OldMainDestination { fun navigationRoute() = ROUTE } +@Deprecated(message = "Part of the old UI") fun NavGraphBuilder.composableOldMainPage() { composable( route = OldMainDestination.baseRoute(), @@ -20,6 +22,7 @@ fun NavGraphBuilder.composableOldMainPage() { } } +@Deprecated(message = "Part of the old UI") fun NavHostController.navigateToOldMainPage() { val route = OldMainDestination.navigationRoute() navigate(route = route) diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/overlay/roll/RollHostState.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/overlay/roll/RollHostState.kt new file mode 100644 index 0000000..1615f9e --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/overlay/roll/RollHostState.kt @@ -0,0 +1,75 @@ +package com.pixelized.desktop.lwa.ui.overlay.roll + +import androidx.compose.runtime.Stable +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import com.pixelized.shared.lwa.model.campaign.Campaign +import kotlinx.coroutines.CancellableContinuation +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlin.coroutines.resume + + +/** + * Build on the SnackbarHostState model. + * @see [androidx.compose.material.SnackbarHostState] + */ +@Stable +class RollHostState { + + private val mutex = Mutex() + + private val currentRollAction = mutableStateOf(null) + val rollAction: State get() = currentRollAction + + suspend fun showRollOverlay( + roll: RollAction.RollActionUio, + ): RollResult = mutex.withLock { + try { + return suspendCancellableCoroutine { continuation -> + currentRollAction.value = RollActionImpl( + roll = roll, + continuation = continuation, + ) + } + } finally { + currentRollAction.value = null + } + } + + @Stable + private class RollActionImpl( + override val roll: RollAction.RollActionUio, + private val continuation: CancellableContinuation, + ) : RollAction { + override fun action(result: RollResult) { + if (continuation.isActive) continuation.resume(result) + } + } +} + +@Stable +enum class RollResult { + Dismissed, + CriticalSuccess, + SpecialSuccess, + Success, + Failure, + CriticalFailure, +} + +@Stable +interface RollAction { + val roll: RollActionUio + + fun action(result: RollResult) + + @Stable + data class RollActionUio( + val characterInstanceId: Campaign.CharacterInstance.Id, + val label: String, + val rollAction: String, + val rollSuccessValue: Int?, + ) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/overlay/roll/RollOverlay.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/overlay/roll/RollOverlay.kt new file mode 100644 index 0000000..160a91c --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/overlay/roll/RollOverlay.kt @@ -0,0 +1,87 @@ +package com.pixelized.desktop.lwa.ui.overlay.roll + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.type +import com.pixelized.desktop.lwa.LocalBlurController +import com.pixelized.desktop.lwa.ui.composable.key.KeyHandler +import kotlinx.coroutines.launch +import org.koin.compose.viewmodel.koinViewModel + + +@Composable +fun RollOverlay( + viewModel: RollViewModel = koinViewModel(), + hostState: RollHostState, +) { + val blur = LocalBlurController.current + val scope = rememberCoroutineScope() + + hostState.rollAction.value.let { + LaunchedEffect(it) { + if (it != null) { + blur.show() + viewModel.prepareRoll(roll = it.roll) + } else { + viewModel.cleanRoll() + blur.hide() + } + } + } + + AnimatedContent( + modifier = Modifier.fillMaxSize(), + targetState = hostState.rollAction.value, + transitionSpec = { + val enter = fadeIn() + slideInVertically { 64 } + val exit = fadeOut() + slideOutVertically { 64 } + enter togetherWith exit + }, + ) { roll -> + when (roll) { + null -> Box( + modifier = Modifier.fillMaxSize() + ) + + else -> Box( + modifier = Modifier.fillMaxSize() + ) { + KeyHandler { + when { + it.type == KeyEventType.KeyUp && it.key == Key.Escape -> { + roll.action(result = viewModel.lastRollResult) + true + } + + else -> false + } + } + + RollPage( + viewModel = viewModel, + onDismissRequest = { + roll.action(result = viewModel.lastRollResult) + }, + onRoll = { + scope.launch { + viewModel.roll() + } + } + ) + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/roll/RollPage.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/overlay/roll/RollPage.kt similarity index 83% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/roll/RollPage.kt rename to composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/overlay/roll/RollPage.kt index bb1aeb5..a4a3995 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/roll/RollPage.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/overlay/roll/RollPage.kt @@ -1,4 +1,4 @@ -package com.pixelized.desktop.lwa.ui.screen.roll +package com.pixelized.desktop.lwa.ui.overlay.roll import androidx.compose.animation.AnimatedContent import androidx.compose.animation.SizeTransform @@ -34,24 +34,17 @@ import androidx.compose.material.icons.filled.ArrowDropDown import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Shadow import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.input.key.Key -import androidx.compose.ui.input.key.KeyEventType -import androidx.compose.ui.input.key.key -import androidx.compose.ui.input.key.type import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import com.pixelized.desktop.lwa.ui.composable.key.KeyHandler -import com.pixelized.desktop.lwa.ui.screen.roll.DifficultyUio.Difficulty +import com.pixelized.desktop.lwa.ui.overlay.roll.DifficultyUio.Difficulty import com.pixelized.desktop.lwa.utils.DisableInteractionSource -import kotlinx.coroutines.launch import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.ic_d20_24dp import lwacharactersheet.composeapp.generated.resources.roll_page__dc__label @@ -63,7 +56,6 @@ import lwacharactersheet.composeapp.generated.resources.roll_page__roll__label import lwacharactersheet.composeapp.generated.resources.roll_page__roll__success_label import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource -import org.koin.compose.viewmodel.koinViewModel @Stable data class RollTitleUio( @@ -89,60 +81,56 @@ data class DifficultyUio( @Composable fun RollPage( - viewModel: RollViewModel = koinViewModel(), + modifier: Modifier = Modifier, + viewModel: RollViewModel, onDismissRequest: () -> Unit, + onRoll: () -> Unit, ) { - val scope = rememberCoroutineScope() - - KeyHandler { - when { - it.type == KeyEventType.KeyUp && it.key == Key.Escape -> { - onDismissRequest() - true - } - else -> { - false - } - } - } - - Column( + Box( modifier = Modifier.fillMaxSize() .clickable( interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = onDismissRequest, ) - .padding(all = 32.dp), - horizontalAlignment = Alignment.CenterHorizontally, + .padding(all = 32.dp) + .then(other = modifier), ) { - Text( - style = MaterialTheme.typography.caption, - textAlign = TextAlign.Center, - overflow = TextOverflow.Ellipsis, - text = stringResource(Res.string.roll_page__roll__label), - ) - Text( - style = MaterialTheme.typography.h5, - textAlign = TextAlign.Center, - overflow = TextOverflow.Ellipsis, - text = viewModel.rollTitle.value.label, - ) - viewModel.rollTitle.value.value?.let { - Text( - style = MaterialTheme.typography.caption, - textAlign = TextAlign.Center, - overflow = TextOverflow.Ellipsis, - text = stringResource(Res.string.roll_page__roll__success_label, it), - ) + viewModel.rollTitle.value?.let { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + style = MaterialTheme.typography.caption, + textAlign = TextAlign.Center, + overflow = TextOverflow.Ellipsis, + text = stringResource(Res.string.roll_page__roll__label), + ) + Text( + style = MaterialTheme.typography.h5, + textAlign = TextAlign.Center, + overflow = TextOverflow.Ellipsis, + text = it.label, + ) + if (it.value != null) { + Text( + style = MaterialTheme.typography.caption, + textAlign = TextAlign.Center, + overflow = TextOverflow.Ellipsis, + text = stringResource(Res.string.roll_page__roll__success_label, it.value), + ) + } + } } + Column( - modifier = Modifier.weight(weight = 1f), + modifier = Modifier.matchParentSize(), + horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy( space = 24.dp, alignment = Alignment.CenterVertically, ), - horizontalAlignment = Alignment.CenterHorizontally, ) { Box( modifier = Modifier.graphicsLayer { @@ -154,7 +142,12 @@ fun RollPage( Icon( modifier = Modifier .clip(shape = CircleShape) - .clickable { scope.launch { viewModel.roll() } } + .let { + when (viewModel.cancellable.value) { + true -> it.clickable(onClick = onRoll) + else -> it + } + } .padding(all = 24.dp) .size(size = 128.dp) .graphicsLayer { @@ -200,13 +193,16 @@ fun RollPage( ) } } + viewModel.rollDifficulty.value?.let { Box( - modifier = Modifier.clickable( - interactionSource = remember { DisableInteractionSource() }, - indication = null, - onClick = {}, - ) + modifier = Modifier + .align(Alignment.BottomCenter) + .clickable( + interactionSource = remember { DisableInteractionSource() }, + indication = null, + onClick = { }, + ) ) { Difficulty( difficulty = it, @@ -224,7 +220,7 @@ fun Difficulty( modifier: Modifier = Modifier, difficulty: DifficultyUio, onToggle: () -> Unit, - onDifficulty: (Difficulty) -> Unit + onDifficulty: (Difficulty) -> Unit, ) { ExposedDropdownMenuBox( modifier = modifier, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/overlay/roll/RollViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/overlay/roll/RollViewModel.kt new file mode 100644 index 0000000..34102d0 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/overlay/roll/RollViewModel.kt @@ -0,0 +1,301 @@ +package com.pixelized.desktop.lwa.ui.overlay.roll + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.ViewModel +import com.pixelized.desktop.lwa.repository.alteration.AlterationRepository +import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository +import com.pixelized.desktop.lwa.repository.network.NetworkRepository +import com.pixelized.desktop.lwa.ui.overlay.roll.DifficultyUio.Difficulty +import com.pixelized.desktop.lwa.ui.overlay.roll.RollAction.RollActionUio +import com.pixelized.shared.lwa.model.AlteredCharacterSheet +import com.pixelized.shared.lwa.model.AlteredCharacterSheetFactory +import com.pixelized.shared.lwa.protocol.websocket.payload.RollMessage +import com.pixelized.shared.lwa.usecase.ExpressionUseCase +import com.pixelized.shared.lwa.usecase.SkillStepUseCase +import kotlinx.coroutines.Job +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import lwacharactersheet.composeapp.generated.resources.Res +import lwacharactersheet.composeapp.generated.resources.roll_page__critical_failure +import lwacharactersheet.composeapp.generated.resources.roll_page__critical_success +import lwacharactersheet.composeapp.generated.resources.roll_page__dc_easy__label +import lwacharactersheet.composeapp.generated.resources.roll_page__dc_hard__label +import lwacharactersheet.composeapp.generated.resources.roll_page__dc_impossible__label +import lwacharactersheet.composeapp.generated.resources.roll_page__dc_normal__label +import lwacharactersheet.composeapp.generated.resources.roll_page__failure +import lwacharactersheet.composeapp.generated.resources.roll_page__special_success +import lwacharactersheet.composeapp.generated.resources.roll_page__success +import org.jetbrains.compose.resources.getString + +class RollViewModel( + private val characterSheetRepository: CharacterSheetRepository, + private val alterationRepository: AlterationRepository, + private val skillComputation: ExpressionUseCase, + private val skillStepUseCase: SkillStepUseCase, + private val networkRepository: NetworkRepository, + private val alteredCharacterSheetFactory: AlteredCharacterSheetFactory, +) : ViewModel() { + private var alteredCharacterSheet: AlteredCharacterSheet? = null + private var rollAction: String? = null + private var rollSuccessValue: Int? = null + + var lastRollResult: RollResult = RollResult.Dismissed + private set + + private var rollJob: Job? = null + + val rollRotation = Animatable(0f) + val rollScale = Animatable(1f) + + private val _cancellable = mutableStateOf(true) + val cancellable: State get() = _cancellable + + private val _rollTitle = mutableStateOf(null) + val rollTitle: State get() = _rollTitle + + private val _rollResult = mutableStateOf(null) + val rollResult: State get() = _rollResult + + private val _rollDifficulty = mutableStateOf(null) + val rollDifficulty: State get() = _rollDifficulty + + suspend fun cleanRoll() { + alteredCharacterSheet = null + rollAction = null + rollSuccessValue = null + + lastRollResult = RollResult.Dismissed + + rollRotation.snapTo(0f) + rollScale.snapTo(1f) + + _cancellable.value = true + _rollTitle.value = null + _rollResult.value = null + _rollDifficulty.value = null + } + + /** + * prepare the this ViewModel to roll. + * return true if the viewModel is ready to roll, otherwise false. + */ + suspend fun prepareRoll( + roll: RollActionUio, + ) { + rollRotation.snapTo(0f) + rollScale.snapTo(1f) + lastRollResult = RollResult.Dismissed + + val characterSheet = characterSheetRepository.characterDetail( + characterSheetId = roll.characterInstanceId.characterSheetId, + ) + + if (characterSheet == null) return + + val alterations = alterationRepository.alterations( + characterInstanceId = roll.characterInstanceId, + ) + + this.alteredCharacterSheet = alteredCharacterSheetFactory.sheet( + characterSheet = characterSheet, + alterations = alterations, + ) + + this.rollAction = roll.rollAction + this.rollSuccessValue = roll.rollSuccessValue + + val rollStep = rollSuccessValue?.let { + skillStepUseCase.computeSkillStep(skill = it) + } + + _cancellable.value = true + _rollResult.value = null + _rollTitle.value = RollTitleUio( + label = roll.label, + value = rollStep?.success?.last + ) + _rollDifficulty.value = rollSuccessValue?.let { + DifficultyUio( + open = false, + difficulty = Difficulty.NORMAL, + ) + } + } + + suspend fun roll() { + if (!cancellable.value) return + val alteredCharacterSheet = alteredCharacterSheet ?: return + val rollAction = rollAction ?: return + val rollTitle = _rollTitle.value ?: return + + coroutineScope { + _rollResult.value = null + + rollJob?.cancel() + rollJob = launch { + launch { + diceScaleAnimation() + } + launch { + diceRotationAnimation() + } + launch { + delay(500) + _cancellable.value = false + // compute the skill critical success to critical failure ranges. + val rollStep = rollSuccessValue?.let { + skillStepUseCase.computeSkillStep( + skill = when (_rollDifficulty.value?.difficulty) { + Difficulty.EASY -> it * 2 + Difficulty.NORMAL -> it + Difficulty.HARD -> it / 2 + Difficulty.IMPOSSIBLE -> it / 4 + else -> it + } + ) + } + + // compute the roll (typically use the expression inside the rollAction) + val roll = skillComputation.computeRoll( + sheet = alteredCharacterSheet, + expression = rollAction, + ) + + // check where the roll fall into the rollSteps. + val resultLabel = rollStep?.let { + when (roll) { + in it.criticalSuccess -> getString(resource = Res.string.roll_page__critical_success) + in it.specialSuccess -> getString(resource = Res.string.roll_page__special_success) + in it.success -> getString(resource = Res.string.roll_page__success) + in it.failure -> getString(resource = Res.string.roll_page__failure) + in it.criticalFailure -> getString(resource = Res.string.roll_page__critical_failure) + else -> "" + } + } + + lastRollResult = rollStep?.let { + when (roll) { + in it.criticalSuccess -> RollResult.CriticalSuccess + in it.specialSuccess -> RollResult.SpecialSuccess + in it.success -> RollResult.Success + in it.failure -> RollResult.Failure + in it.criticalFailure -> RollResult.CriticalFailure + else -> RollResult.Dismissed + } + } ?: RollResult.Dismissed + + _rollResult.value = RollResultUio( + label = resultLabel ?: "", + value = roll, + ) + + launch { + shareRollResult( + alteredCharacterSheet = alteredCharacterSheet, + rollTitle = rollTitle, + roll = roll, + rollStep = rollStep, + success = resultLabel + ) + } + } + } + } + } + + fun toggleDifficulty() { + _rollDifficulty.value = _rollDifficulty.value?.copy( + open = _rollDifficulty.value?.open?.not() ?: false + ) + } + + fun onDifficulty(difficulty: Difficulty) { + _rollDifficulty.value = DifficultyUio( + open = false, + difficulty = difficulty, + ) + val rollStep = rollSuccessValue?.let { + skillStepUseCase.computeSkillStep( + skill = when (_rollDifficulty.value?.difficulty) { + Difficulty.EASY -> it * 2 + Difficulty.NORMAL -> it + Difficulty.HARD -> it / 2 + Difficulty.IMPOSSIBLE -> it / 4 + else -> it + } + ) + } + _rollTitle.value = _rollTitle.value?.copy( + value = rollStep?.success?.last + ) + } + + private suspend fun diceRotationAnimation() { + rollRotation.animateTo( + targetValue = rollRotation.value.let { it - it % 360 } + 360f * 3, + animationSpec = spring( + dampingRatio = Spring.DampingRatioNoBouncy, + stiffness = Spring.StiffnessLow, + ) + ) + } + + private suspend fun diceScaleAnimation() { + rollScale.animateTo( + targetValue = 1.20f, + animationSpec = spring( + dampingRatio = Spring.DampingRatioNoBouncy, + stiffness = 800f, + ) + ) + rollScale.animateTo( + targetValue = 1f, + animationSpec = spring( + dampingRatio = 0.28f, + stiffness = 800f, + ) + ) + } + + private suspend fun shareRollResult( + alteredCharacterSheet: AlteredCharacterSheet, + rollTitle: RollTitleUio, + roll: Int, + rollStep: SkillStepUseCase.SkillStep?, + success: String?, + ) { + val payload = RollMessage( + id = RollMessage.RollId.create(), + characterSheetId = alteredCharacterSheet.id, + skillLabel = rollTitle.label, + rollValue = roll, + resultLabel = success, + rollDifficulty = when (_rollDifficulty.value?.difficulty) { + Difficulty.EASY -> getString(Res.string.roll_page__dc_easy__label) + Difficulty.NORMAL -> getString(Res.string.roll_page__dc_normal__label) + Difficulty.HARD -> getString(Res.string.roll_page__dc_hard__label) + Difficulty.IMPOSSIBLE -> getString(Res.string.roll_page__dc_impossible__label) + else -> null + }, + rollSuccessLimit = rollStep?.success?.last, + critical = rollStep?.let { + when (roll) { + in it.criticalSuccess -> RollMessage.Critical.CRITICAL_SUCCESS + in it.specialSuccess -> RollMessage.Critical.SPECIAL_SUCCESS + in it.success -> RollMessage.Critical.SUCCESS + in it.failure -> RollMessage.Critical.FAILURE + in it.criticalFailure -> RollMessage.Critical.CRITICAL_FAILURE + else -> null + } + }, + ) + networkRepository.share( + payload = payload, + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/CampaignScreen.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/CampaignScreen.kt index 1bd2842..4064fe6 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/CampaignScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/CampaignScreen.kt @@ -1,11 +1,5 @@ package com.pixelized.desktop.lwa.ui.screen.campaign -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutVertically -import androidx.compose.animation.togetherWith import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxHeight @@ -38,6 +32,8 @@ import com.pixelized.desktop.lwa.ui.composable.blur.rememberBlurContentControlle import com.pixelized.desktop.lwa.ui.composable.character.characteristic.CharacterDetailCharacteristicDialogViewModel import com.pixelized.desktop.lwa.ui.composable.character.characteristic.CharacterSheetCharacteristicDialog import com.pixelized.desktop.lwa.ui.composable.key.KeyHandler +import com.pixelized.desktop.lwa.ui.navigation.screen.LocalScreenController +import com.pixelized.desktop.lwa.ui.navigation.screen.destination.navigateToLevelScreen import com.pixelized.desktop.lwa.ui.screen.campaign.chat.CampaignChat import com.pixelized.desktop.lwa.ui.screen.campaign.chat.CampaignChatViewModel import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailPanel @@ -47,8 +43,9 @@ import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.PlayerRibbon import com.pixelized.desktop.lwa.ui.screen.campaign.toolbar.CampaignToolbar import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.dialog.DiminishedStatDialog import com.pixelized.desktop.lwa.ui.screen.network.NetworkViewModel -import com.pixelized.desktop.lwa.ui.screen.roll.RollPage -import com.pixelized.desktop.lwa.ui.screen.roll.RollViewModel +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.RollViewModel import kotlinx.coroutines.launch import org.koin.compose.viewmodel.koinViewModel @@ -57,7 +54,7 @@ val LocalCampaignLayoutScope = compositionLocalOf { } @Composable -fun MainPage( +fun CampaignScreen( characterDetailViewModel: CharacterDetailViewModel = koinViewModel(), characteristicDialogViewModel: CharacterDetailCharacteristicDialogViewModel = koinViewModel(), dismissedViewModel: CharacterDiminishedViewModel = koinViewModel(), @@ -66,6 +63,11 @@ fun MainPage( campaignChatViewModel: CampaignChatViewModel = koinViewModel(), rollViewModel: RollViewModel = koinViewModel(), ) { + val screen = LocalScreenController.current + val blurController = rememberBlurContentController() + val scope = rememberCoroutineScope() + + KeyHandler { when { it.type == KeyEventType.KeyUp && it.key == Key.Escape -> { @@ -77,9 +79,6 @@ fun MainPage( } } - val scope = rememberCoroutineScope() - val blurController = rememberBlurContentController() - Surface( modifier = Modifier.fillMaxSize(), ) { @@ -113,6 +112,9 @@ fun MainPage( onCharacter = { characterDetailViewModel.showCharacter(id = it) }, + onLevelUp = { + screen.navigateToLevelScreen(characterInstanceId = it) + } ) }, rightOverlay = { @@ -123,7 +125,6 @@ fun MainPage( .fillMaxHeight(), blurController = blurController, detailViewModel = characterDetailViewModel, - rollViewModel = rollViewModel, characterDiminishedViewModel = dismissedViewModel, characteristicDialogViewModel = characteristicDialogViewModel, ) @@ -131,30 +132,6 @@ fun MainPage( ) } - AnimatedContent( - modifier = Modifier.fillMaxSize(), - targetState = rollViewModel.displayOverlay.value, - transitionSpec = { - val enter = fadeIn() + slideInVertically { 64 } - val exit = fadeOut() + slideOutVertically { 64 } - enter togetherWith exit - }, - ) { roll -> - when (roll) { - true -> RollPage( - viewModel = rollViewModel, - onDismissRequest = { - blurController.hide() - rollViewModel.hideOverlay() - }, - ) - - else -> Box( - modifier = Modifier.fillMaxSize() - ) - } - } - CharacterSheetCharacteristicDialog( dialog = characteristicDialogViewModel.statChangeDialog, onConfirm = { dialog -> diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/chat/TextMessageFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/chat/TextMessageFactory.kt index b7674a5..5f499ba 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/chat/TextMessageFactory.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/chat/TextMessageFactory.kt @@ -38,11 +38,11 @@ class TextMessageFactory( return when (val payload = message.value) { is RollMessage -> { val sheetPreview = characterSheetRepository - .characterPreview(characterId = payload.characterId) + .characterPreview(characterId = payload.characterSheetId) ?: return null RollTextMessageUio( - id = id, + id = "${payload.id.rollId}-${payload.id.timestamp}", timestamp = formatTime.format(time), character = sheetPreview.name, skillLabel = payload.skillLabel, @@ -62,17 +62,12 @@ class TextMessageFactory( } is CampaignMessage.UpdateDiminished -> { - val characterInstanceId = Campaign.CharacterInstance.Id( - characterSheetId = payload.characterSheetId, - instanceId = payload.instanceId, - ) - val sheetPreview = characterSheetRepository .characterPreview(characterId = payload.characterSheetId) ?: return null DiminishedTextMessageUio( - id = id, + id = "${message.from}-$id-Diminished", timestamp = formatTime.format(time), character = sheetPreview.name, diminished = payload.diminished, @@ -95,7 +90,7 @@ class TextMessageFactory( alterations = alterations, ) CharacteristicTextMessageUio( - id = id, + id = "${message.from}-$id-Characteristic", timestamp = formatTime.format(time), character = sheet.name, value = when (payload.characteristic) { diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDetail.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDetail.kt index 22f5bd0..634c85f 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDetail.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDetail.kt @@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface @@ -23,13 +22,14 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Shape import androidx.compose.ui.unit.dp +import com.pixelized.desktop.lwa.LocalRollHostState import com.pixelized.desktop.lwa.ui.composable.blur.BlurContentController import com.pixelized.desktop.lwa.ui.composable.character.characteristic.CharacterDetailCharacteristicDialogViewModel +import com.pixelized.desktop.lwa.ui.overlay.roll.RollViewModel import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.header.CharacterDetailHeader import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.header.CharacterDetailHeaderUio import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.CharacterDetailSheet @@ -37,7 +37,6 @@ import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.Characte import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.CharacterDetailSheetCharacteristicUio import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.CharacterDetailSheetSkillUio import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.CharacterDetailSheetUio -import com.pixelized.desktop.lwa.ui.screen.roll.RollViewModel import com.pixelized.desktop.lwa.ui.theme.lwa import com.pixelized.shared.lwa.model.campaign.Campaign import kotlinx.coroutines.flow.StateFlow @@ -57,8 +56,8 @@ fun CharacterDetailPanel( detailViewModel: CharacterDetailViewModel, characteristicDialogViewModel: CharacterDetailCharacteristicDialogViewModel, characterDiminishedViewModel: CharacterDiminishedViewModel, - rollViewModel: RollViewModel, ) { + val roll = LocalRollHostState.current val scope = rememberCoroutineScope() val detail: State = detailViewModel.detail.collectAsState() @@ -95,14 +94,16 @@ fun CharacterDetailPanel( } }, onCharacteristic = { - rollViewModel.prepareRoll(roll = it.roll) - rollViewModel.showOverlay() - blurController.show() + scope.launch { + val result = roll.showRollOverlay(roll = it.roll) + println("result: $result") + } }, onSkill = { - rollViewModel.prepareRoll(roll = it.roll) - rollViewModel.showOverlay() - blurController.show() + scope.launch { + val result = roll.showRollOverlay(roll = it.roll) + println("result: $result") + } }, onUseSkill = { scope.launch { @@ -113,9 +114,10 @@ fun CharacterDetailPanel( } }, onAction = { - rollViewModel.prepareRoll(roll = it.roll) - rollViewModel.showOverlay() - blurController.show() + scope.launch { + val result = roll.showRollOverlay(roll = it.roll) + println("result: $result") + } } ) @@ -182,7 +184,7 @@ fun CharacterDetailAnimatedPanel( @Composable fun CharacterDetailContent( modifier: Modifier = Modifier, - shape: Shape = remember { RoundedCornerShape(16.dp) }, + shape: Shape = MaterialTheme.lwa.shapes.panel, header: State, sheet: State, onDismissRequest: () -> Unit, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDetailFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDetailFactory.kt index b4ae7fc..ea55f28 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDetailFactory.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDetailFactory.kt @@ -1,12 +1,12 @@ package com.pixelized.desktop.lwa.ui.screen.campaign.player.detail import com.pixelized.desktop.lwa.ui.composable.tooltip.TooltipUio +import com.pixelized.desktop.lwa.ui.overlay.roll.RollAction.RollActionUio import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.header.CharacterDetailHeaderUio import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.CharacterDetailSheetActionUio import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.CharacterDetailSheetCharacteristicUio import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.CharacterDetailSheetSkillUio import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.CharacterDetailSheetUio -import com.pixelized.desktop.lwa.ui.screen.roll.RollActionUio import com.pixelized.shared.lwa.model.AlteredCharacterSheetFactory import com.pixelized.shared.lwa.model.alteration.FieldAlteration import com.pixelized.shared.lwa.model.campaign.Campaign @@ -22,6 +22,13 @@ import lwacharactersheet.composeapp.generated.resources.character_sheet__charact import lwacharactersheet.composeapp.generated.resources.character_sheet__characteristics__int import lwacharactersheet.composeapp.generated.resources.character_sheet__characteristics__pow import lwacharactersheet.composeapp.generated.resources.character_sheet__characteristics__str +import lwacharactersheet.composeapp.generated.resources.character_sheet__sub_characteristics__armor +import lwacharactersheet.composeapp.generated.resources.character_sheet__sub_characteristics__damage_bonus +import lwacharactersheet.composeapp.generated.resources.character_sheet__sub_characteristics__hit_point +import lwacharactersheet.composeapp.generated.resources.character_sheet__sub_characteristics__hp_grow +import lwacharactersheet.composeapp.generated.resources.character_sheet__sub_characteristics__learning +import lwacharactersheet.composeapp.generated.resources.character_sheet__sub_characteristics__movement +import lwacharactersheet.composeapp.generated.resources.character_sheet__sub_characteristics__power_point import lwacharactersheet.composeapp.generated.resources.tooltip__characteristics__charisma import lwacharactersheet.composeapp.generated.resources.tooltip__characteristics__constitution import lwacharactersheet.composeapp.generated.resources.tooltip__characteristics__dexterity @@ -29,6 +36,13 @@ import lwacharactersheet.composeapp.generated.resources.tooltip__characteristics import lwacharactersheet.composeapp.generated.resources.tooltip__characteristics__intelligence import lwacharactersheet.composeapp.generated.resources.tooltip__characteristics__power import lwacharactersheet.composeapp.generated.resources.tooltip__characteristics__strength +import lwacharactersheet.composeapp.generated.resources.tooltip__sub_characteristics__armor +import lwacharactersheet.composeapp.generated.resources.tooltip__sub_characteristics__bonus_damage +import lwacharactersheet.composeapp.generated.resources.tooltip__sub_characteristics__hit_point +import lwacharactersheet.composeapp.generated.resources.tooltip__sub_characteristics__hp_grow +import lwacharactersheet.composeapp.generated.resources.tooltip__sub_characteristics__learning +import lwacharactersheet.composeapp.generated.resources.tooltip__sub_characteristics__movement +import lwacharactersheet.composeapp.generated.resources.tooltip__sub_characteristics__power_point import org.jetbrains.compose.resources.getString import java.text.Collator @@ -36,7 +50,7 @@ class CharacterDetailFactory( private val alteredCharacterSheetFactory: AlteredCharacterSheetFactory, private val expressionUseCase: ExpressionUseCase, ) { - fun convertToCharacterDetailHeaderUio( + suspend fun convertToCharacterDetailHeaderUio( characterInstanceId: Campaign.CharacterInstance.Id, characterSheet: CharacterSheet?, characterInstance: Campaign.CharacterInstance, @@ -57,15 +71,44 @@ class CharacterDetailFactory( portrait = alteredCharacterSheet.portrait, diminished = characterInstance.diminished, name = alteredCharacterSheet.name, + level = alteredCharacterSheet.level, hp = "${maxHp - characterInstance.damage}", + hpTooltip = TooltipUio( + title = getString(Res.string.character_sheet__sub_characteristics__hit_point), + description = getString(Res.string.tooltip__sub_characteristics__hit_point) + ), maxHp = "$maxHp", pp = "${maxPp - characterInstance.power}", + ppTooltip = TooltipUio( + title = getString(Res.string.character_sheet__sub_characteristics__power_point), + description = getString(Res.string.tooltip__sub_characteristics__power_point) + ), maxPp = "$maxPp", mov = "${alteredCharacterSheet.movement}", + movTooltip = TooltipUio( + title = getString(Res.string.character_sheet__sub_characteristics__movement), + description = getString(Res.string.tooltip__sub_characteristics__movement) + ), armor = "${alteredCharacterSheet.armor}", + armorTooltip = TooltipUio( + title = getString(Res.string.character_sheet__sub_characteristics__armor), + description = getString(Res.string.tooltip__sub_characteristics__armor) + ), bonus = alteredCharacterSheet.damageBonus, + bonusTooltip = TooltipUio( + title = getString(Res.string.character_sheet__sub_characteristics__damage_bonus), + description = getString(Res.string.tooltip__sub_characteristics__bonus_damage) + ), grow = "${alteredCharacterSheet.hpGrow}", + growTooltip = TooltipUio( + title = getString(Res.string.character_sheet__sub_characteristics__hp_grow), + description = getString(Res.string.tooltip__sub_characteristics__hp_grow) + ), learn = "${alteredCharacterSheet.learning}", + learnTooltip = TooltipUio( + title = getString(Res.string.character_sheet__sub_characteristics__learning), + description = getString(Res.string.tooltip__sub_characteristics__learning) + ), ) } @@ -93,7 +136,7 @@ class CharacterDetailFactory( description = getString(Res.string.tooltip__characteristics__strength), ), roll = RollActionUio( - characterSheetId = characterSheet.id, + characterInstanceId = characterInstanceId, label = getString(Res.string.character_sheet__characteristics__str), rollAction = "1d100", rollSuccessValue = alteredCharacterSheet.strength * 5 - characterInstance.diminished, @@ -107,7 +150,7 @@ class CharacterDetailFactory( description = getString(Res.string.tooltip__characteristics__dexterity), ), roll = RollActionUio( - characterSheetId = characterSheet.id, + characterInstanceId = characterInstanceId, label = getString(Res.string.character_sheet__characteristics__dex), rollAction = "1d100", rollSuccessValue = alteredCharacterSheet.dexterity * 5 - characterInstance.diminished, @@ -121,7 +164,7 @@ class CharacterDetailFactory( description = getString(Res.string.tooltip__characteristics__constitution), ), roll = RollActionUio( - characterSheetId = characterSheet.id, + characterInstanceId = characterInstanceId, label = getString(Res.string.character_sheet__characteristics__con), rollAction = "1d100", rollSuccessValue = alteredCharacterSheet.constitution * 5 - characterInstance.diminished, @@ -135,7 +178,7 @@ class CharacterDetailFactory( description = getString(Res.string.tooltip__characteristics__height), ), roll = RollActionUio( - characterSheetId = characterSheet.id, + characterInstanceId = characterInstanceId, label = getString(Res.string.character_sheet__characteristics__hei), rollAction = "1d100", rollSuccessValue = alteredCharacterSheet.height * 5 - characterInstance.diminished, @@ -149,7 +192,7 @@ class CharacterDetailFactory( description = getString(Res.string.tooltip__characteristics__intelligence), ), roll = RollActionUio( - characterSheetId = characterSheet.id, + characterInstanceId = characterInstanceId, label = getString(Res.string.character_sheet__characteristics__int), rollAction = "1d100", rollSuccessValue = alteredCharacterSheet.intelligence * 5 - characterInstance.diminished, @@ -163,7 +206,7 @@ class CharacterDetailFactory( description = getString(Res.string.tooltip__characteristics__power), ), roll = RollActionUio( - characterSheetId = characterSheet.id, + characterInstanceId = characterInstanceId, label = getString(Res.string.character_sheet__characteristics__pow), rollAction = "1d100", rollSuccessValue = alteredCharacterSheet.power * 5 - characterInstance.diminished, @@ -177,7 +220,7 @@ class CharacterDetailFactory( description = getString(Res.string.tooltip__characteristics__charisma), ), roll = RollActionUio( - characterSheetId = characterSheet.id, + characterInstanceId = characterInstanceId, label = getString(Res.string.character_sheet__characteristics__cha), rollAction = "1d100", rollSuccessValue = alteredCharacterSheet.charisma * 5 - characterInstance.diminished, @@ -186,7 +229,7 @@ class CharacterDetailFactory( ), commonSkills = characterSheet.commonSkills.map { skill -> val value = expressionUseCase.computeSkillValue( - sheet = characterSheet, + sheet = alteredCharacterSheet, skill = skill, diminished = characterInstance.diminished, alterations = alterations, @@ -204,7 +247,7 @@ class CharacterDetailFactory( ) }, roll = RollActionUio( - characterSheetId = characterInstanceId.characterSheetId, + characterInstanceId = characterInstanceId, label = skill.label, rollAction = "1d100", rollSuccessValue = value, @@ -213,7 +256,7 @@ class CharacterDetailFactory( }.sortedWith(compareBy(Collator.getInstance()) { it.label }), characterSheet.specialSkills.map { skill -> val value = expressionUseCase.computeSkillValue( - sheet = characterSheet, + sheet = alteredCharacterSheet, skill = skill, diminished = characterInstance.diminished, alterations = alterations, @@ -231,7 +274,7 @@ class CharacterDetailFactory( ) }, roll = RollActionUio( - characterSheetId = characterInstanceId.characterSheetId, + characterInstanceId = characterInstanceId, label = skill.label, rollAction = "1d100", rollSuccessValue = value, @@ -240,7 +283,7 @@ class CharacterDetailFactory( }.sortedWith(compareBy(Collator.getInstance()) { it.label }), magicSkills = characterSheet.magicSkills.map { skill -> val value = expressionUseCase.computeSkillValue( - sheet = characterSheet, + sheet = alteredCharacterSheet, skill = skill, diminished = characterInstance.diminished, alterations = alterations, @@ -258,7 +301,7 @@ class CharacterDetailFactory( ) }, roll = RollActionUio( - characterSheetId = characterInstanceId.characterSheetId, + characterInstanceId = characterInstanceId, label = skill.label, rollAction = "1d100", rollSuccessValue = value, @@ -276,7 +319,7 @@ class CharacterDetailFactory( ) }, roll = RollActionUio( - characterSheetId = characterInstanceId.characterSheetId, + characterInstanceId = characterInstanceId, label = action.label, rollAction = action.roll, rollSuccessValue = null, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDetailViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDetailViewModel.kt index f37871b..ebff38d 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDetailViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDetailViewModel.kt @@ -32,8 +32,8 @@ class CharacterDetailViewModel( characterInstanceId = characterInstanceId, header = combine( campaignRepository.characterInstanceFlow(id = characterInstanceId), - characterSheetRepository.characterDetailFlow(characterId = characterInstanceId.characterSheetId), - alterationRepository.alterationsFlow(characterId = characterInstanceId), + characterSheetRepository.characterDetailFlow(characterSheetId = characterInstanceId.characterSheetId), + alterationRepository.alterationsFlow(characterInstanceId = characterInstanceId), ) { characterInstance, characterSheet, alterations -> characterDetailFactory.convertToCharacterDetailHeaderUio( characterInstanceId = characterInstanceId, @@ -48,8 +48,8 @@ class CharacterDetailViewModel( ), sheet = combine( campaignRepository.characterInstanceFlow(id = characterInstanceId), - characterSheetRepository.characterDetailFlow(characterId = characterInstanceId.characterSheetId), - alterationRepository.alterationsFlow(characterId = characterInstanceId), + characterSheetRepository.characterDetailFlow(characterSheetId = characterInstanceId.characterSheetId), + alterationRepository.alterationsFlow(characterInstanceId = characterInstanceId), ) { characterInstance, characterSheet, alterations -> characterDetailFactory.convertToCharacterDetailSheetUio( characterInstanceId = characterInstanceId, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/header/CharacterDetailHeader.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/header/CharacterDetailHeader.kt index 495d330..27000df 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/header/CharacterDetailHeader.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/header/CharacterDetailHeader.kt @@ -7,13 +7,13 @@ import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically import androidx.compose.animation.togetherWith +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width @@ -22,7 +22,6 @@ import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme import androidx.compose.material.Text -import androidx.compose.material.minimumInteractiveComponentSize import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.State @@ -34,9 +33,12 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import com.pixelized.desktop.lwa.ui.composable.tooltip.TooltipLayout +import com.pixelized.desktop.lwa.ui.composable.tooltip.TooltipUio import com.pixelized.desktop.lwa.ui.theme.lwa import com.pixelized.shared.lwa.model.campaign.Campaign import lwacharactersheet.composeapp.generated.resources.Res +import lwacharactersheet.composeapp.generated.resources.character_sheet__level import lwacharactersheet.composeapp.generated.resources.ic_close_24dp import lwacharactersheet.composeapp.generated.resources.ic_cognition_24dp import lwacharactersheet.composeapp.generated.resources.ic_heart_24dp @@ -47,6 +49,7 @@ import lwacharactersheet.composeapp.generated.resources.ic_skull_24dp import lwacharactersheet.composeapp.generated.resources.ic_swords_24dp import lwacharactersheet.composeapp.generated.resources.ic_water_drop_24dp import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.resources.stringResource @Stable data class CharacterDetailHeaderUio( @@ -54,17 +57,26 @@ data class CharacterDetailHeaderUio( val portrait: String?, val diminished: Int, val name: String, + val level: Int, val hp: String, + val hpTooltip: TooltipUio, val maxHp: String, val pp: String, + val ppTooltip: TooltipUio, val maxPp: String, val mov: String, + val movTooltip: TooltipUio, val armor: String, + val armorTooltip: TooltipUio, val bonus: String, + val bonusTooltip: TooltipUio, val grow: String, + val growTooltip: TooltipUio, val learn: String, + val learnTooltip: TooltipUio, ) +@OptIn(ExperimentalFoundationApi::class) @Composable fun CharacterDetailHeader( modifier: Modifier = Modifier, @@ -79,14 +91,28 @@ fun CharacterDetailHeader( modifier = modifier, ) { Row(modifier = Modifier.padding(start = 16.dp)) { - Text( - modifier = Modifier.weight(1f) + Row( + modifier = Modifier + .weight(1f) .align(alignment = Alignment.CenterVertically), - style = MaterialTheme.typography.h5, - text = header.value?.name ?: "", - overflow = TextOverflow.Ellipsis, - maxLines = 1, - ) + horizontalArrangement = Arrangement.spacedBy(space = 4.dp), + ) { + Text( + modifier = Modifier.alignByBaseline(), + style = MaterialTheme.typography.h5, + text = header.value?.name ?: "", + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + Text( + modifier = Modifier.alignByBaseline(), + style = MaterialTheme.lwa.typography.base.caption, + text = stringResource( + resource = Res.string.character_sheet__level, + header.value?.level ?: 0 + ), + ) + } Box { IconButton( onClick = onDiminished, @@ -131,143 +157,171 @@ fun CharacterDetailHeader( modifier = Modifier.padding(horizontal = 16.dp), horizontalArrangement = Arrangement.spacedBy(space = 16.dp), ) { - Row( - modifier = Modifier.clip(shape = CircleShape).clickable { onHp() }, - verticalAlignment = Alignment.Bottom, + TooltipLayout( + tooltip = header.value?.hpTooltip, ) { - Icon( - modifier = Modifier - .padding(bottom = 4.dp, end = 2.dp) - .size(size = iconSize), - painter = painterResource(Res.drawable.ic_heart_24dp), - contentDescription = null - ) - Text( - modifier = Modifier.alignByBaseline(), - style = MaterialTheme.typography.h6, - color = MaterialTheme.lwa.colorScheme.base.primary, - fontWeight = FontWeight.Bold, - text = header.value?.hp ?: "", - ) - Text( - modifier = Modifier.alignByBaseline(), - style = MaterialTheme.typography.caption, - fontWeight = FontWeight.Thin, - text = "/${header.value?.maxHp ?: ""}", - ) + Row( + modifier = Modifier.clip(shape = CircleShape).clickable { onHp() }, + verticalAlignment = Alignment.Bottom, + ) { + Icon( + modifier = Modifier + .padding(bottom = 4.dp, end = 2.dp) + .size(size = iconSize), + painter = painterResource(Res.drawable.ic_heart_24dp), + contentDescription = null + ) + Text( + modifier = Modifier.alignByBaseline(), + style = MaterialTheme.typography.h6, + color = MaterialTheme.lwa.colorScheme.base.primary, + fontWeight = FontWeight.Bold, + text = header.value?.hp ?: "", + ) + Text( + modifier = Modifier.alignByBaseline(), + style = MaterialTheme.typography.caption, + fontWeight = FontWeight.Thin, + text = "/${header.value?.maxHp ?: ""}", + ) + } } - Row( - modifier = Modifier.clip(shape = CircleShape).clickable { onPp() }, - verticalAlignment = Alignment.Bottom, + TooltipLayout( + tooltip = header.value?.ppTooltip, ) { - Icon( - modifier = Modifier - .padding(bottom = 4.dp, end = 2.dp) - .size(size = iconSize), - painter = painterResource(Res.drawable.ic_water_drop_24dp), - contentDescription = null - ) - Text( - modifier = Modifier.alignByBaseline(), - style = MaterialTheme.typography.h6, - color = MaterialTheme.lwa.colorScheme.base.primary, - fontWeight = FontWeight.Bold, - text = header.value?.pp ?: "", - ) - Text( - modifier = Modifier.alignByBaseline(), - style = MaterialTheme.typography.caption, - fontWeight = FontWeight.Thin, - text = "/${header.value?.maxPp ?: ""}", - ) + Row( + modifier = Modifier.clip(shape = CircleShape).clickable { onPp() }, + verticalAlignment = Alignment.Bottom, + ) { + Icon( + modifier = Modifier + .padding(bottom = 4.dp, end = 2.dp) + .size(size = iconSize), + painter = painterResource(Res.drawable.ic_water_drop_24dp), + contentDescription = null + ) + Text( + modifier = Modifier.alignByBaseline(), + style = MaterialTheme.typography.h6, + color = MaterialTheme.lwa.colorScheme.base.primary, + fontWeight = FontWeight.Bold, + text = header.value?.pp ?: "", + ) + Text( + modifier = Modifier.alignByBaseline(), + style = MaterialTheme.typography.caption, + fontWeight = FontWeight.Thin, + text = "/${header.value?.maxPp ?: ""}", + ) + } } Spacer(modifier = Modifier.weight(1f)) - Row( - verticalAlignment = Alignment.Bottom, + TooltipLayout( + tooltip = header.value?.movTooltip, ) { - Icon( - modifier = Modifier - .padding(bottom = 4.dp, end = 2.dp) - .size(size = iconSize), - painter = painterResource(Res.drawable.ic_near_me), - contentDescription = null, - ) - Text( - modifier = Modifier.alignByBaseline(), - style = MaterialTheme.typography.h6, - text = header.value?.mov ?: "", - ) - Text( - modifier = Modifier.alignByBaseline(), - style = MaterialTheme.typography.caption, - text = "m", - ) + Row( + verticalAlignment = Alignment.Bottom, + ) { + Icon( + modifier = Modifier + .padding(bottom = 4.dp, end = 2.dp) + .size(size = iconSize), + painter = painterResource(Res.drawable.ic_near_me), + contentDescription = null, + ) + Text( + modifier = Modifier.alignByBaseline(), + style = MaterialTheme.typography.h6, + text = header.value?.mov ?: "", + ) + Text( + modifier = Modifier.alignByBaseline(), + style = MaterialTheme.typography.caption, + text = "m", + ) + } } - Row( - verticalAlignment = Alignment.Bottom, + TooltipLayout( + tooltip = header.value?.armorTooltip, ) { - Icon( - modifier = Modifier - .padding(bottom = 4.dp, end = 2.dp) - .size(size = iconSize), - painter = painterResource(Res.drawable.ic_shield_24dp), - contentDescription = null, - ) - Text( - modifier = Modifier.alignByBaseline(), - style = MaterialTheme.typography.h6, - text = header.value?.armor ?: "", - ) + Row( + verticalAlignment = Alignment.Bottom, + ) { + Icon( + modifier = Modifier + .padding(bottom = 4.dp, end = 2.dp) + .size(size = iconSize), + painter = painterResource(Res.drawable.ic_shield_24dp), + contentDescription = null, + ) + Text( + modifier = Modifier.alignByBaseline(), + style = MaterialTheme.typography.h6, + text = header.value?.armor ?: "", + ) + } } - Row( - verticalAlignment = Alignment.Bottom, + TooltipLayout( + tooltip = header.value?.bonusTooltip, ) { - Icon( - modifier = Modifier - .padding(bottom = 4.dp, end = 2.dp) - .size(size = iconSize), - painter = painterResource(Res.drawable.ic_swords_24dp), - contentDescription = null, - ) - Text( - modifier = Modifier.alignByBaseline(), - style = MaterialTheme.typography.h6, - text = header.value?.bonus ?: "", - ) + Row( + verticalAlignment = Alignment.Bottom, + ) { + Icon( + modifier = Modifier + .padding(bottom = 4.dp, end = 2.dp) + .size(size = iconSize), + painter = painterResource(Res.drawable.ic_swords_24dp), + contentDescription = null, + ) + Text( + modifier = Modifier.alignByBaseline(), + style = MaterialTheme.typography.h6, + text = header.value?.bonus ?: "", + ) + } } - Row( - verticalAlignment = Alignment.Bottom, + TooltipLayout( + tooltip = header.value?.growTooltip, ) { - Icon( - modifier = Modifier - .padding(bottom = 4.dp, end = 2.dp) - .size(size = iconSize), - painter = painterResource(Res.drawable.ic_heart_plus_24dp), - contentDescription = null, - ) - Text( - modifier = Modifier.alignByBaseline(), - style = MaterialTheme.typography.h6, - text = header.value?.grow ?: "", - ) + Row( + verticalAlignment = Alignment.Bottom, + ) { + Icon( + modifier = Modifier + .padding(bottom = 4.dp, end = 2.dp) + .size(size = iconSize), + painter = painterResource(Res.drawable.ic_heart_plus_24dp), + contentDescription = null, + ) + Text( + modifier = Modifier.alignByBaseline(), + style = MaterialTheme.typography.h6, + text = header.value?.grow ?: "", + ) + } } - Row( - verticalAlignment = Alignment.Bottom, + TooltipLayout( + tooltip = header.value?.learnTooltip, ) { - Icon( - modifier = Modifier - .padding(bottom = 4.dp, end = 2.dp) - .size(size = iconSize), - painter = painterResource(Res.drawable.ic_cognition_24dp), - contentDescription = null, - ) - Text( - modifier = Modifier.alignByBaseline(), - style = MaterialTheme.typography.h6, - text = header.value?.learn ?: "", - ) + Row( + verticalAlignment = Alignment.Bottom, + ) { + Icon( + modifier = Modifier + .padding(bottom = 4.dp, end = 2.dp) + .size(size = iconSize), + painter = painterResource(Res.drawable.ic_cognition_24dp), + contentDescription = null, + ) + Text( + modifier = Modifier.alignByBaseline(), + style = MaterialTheme.typography.h6, + text = header.value?.learn ?: "", + ) + } } } } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/CharacterDetailSheet.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/CharacterDetailSheet.kt index a4207b2..6a273e1 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/CharacterDetailSheet.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/CharacterDetailSheet.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.pixelized.desktop.lwa.ui.composable.decoratedBox.DecoratedBox +import com.pixelized.desktop.lwa.ui.theme.lwa import com.pixelized.shared.lwa.model.campaign.Campaign import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.character_sheet__skills__common_title @@ -51,7 +52,7 @@ fun CharacterDetailSheet( ) { sheet.value?.characteristics?.forEach { CharacterDetailSheetCharacteristic( - modifier = Modifier.size(width = 76.dp, height = 110.dp), + modifier = Modifier.size(size = MaterialTheme.lwa.size.characteristic), characteristic = it, onClick = { onCharacteristic(it) }, ) diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/CharacterDetailSheetAction.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/CharacterDetailSheetAction.kt index 1e19d97..0d76c96 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/CharacterDetailSheetAction.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/CharacterDetailSheetAction.kt @@ -4,7 +4,6 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material.Icon @@ -16,9 +15,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import com.pixelized.desktop.lwa.ui.composable.circle.MasteryShape +import com.pixelized.desktop.lwa.ui.composable.shapes.MasteryShape import com.pixelized.desktop.lwa.ui.composable.tooltip.TooltipUio -import com.pixelized.desktop.lwa.ui.screen.roll.RollActionUio +import com.pixelized.desktop.lwa.ui.overlay.roll.RollAction.RollActionUio import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.ic_d20_24dp import org.jetbrains.compose.resources.painterResource diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/CharacterDetailSheetCharacteristic.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/CharacterDetailSheetCharacteristic.kt index c07de89..da870e2 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/CharacterDetailSheetCharacteristic.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/CharacterDetailSheetCharacteristic.kt @@ -15,7 +15,7 @@ import androidx.compose.ui.unit.dp import com.pixelized.desktop.lwa.ui.composable.decoratedBox.DecoratedBox import com.pixelized.desktop.lwa.ui.composable.tooltip.TooltipLayout import com.pixelized.desktop.lwa.ui.composable.tooltip.TooltipUio -import com.pixelized.desktop.lwa.ui.screen.roll.RollActionUio +import com.pixelized.desktop.lwa.ui.overlay.roll.RollAction.RollActionUio @Stable data class CharacterDetailSheetCharacteristicUio( diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/CharacterDetailSheetSkill.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/CharacterDetailSheetSkill.kt index 3b8d66f..bfd86e3 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/CharacterDetailSheetSkill.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/sheet/CharacterDetailSheetSkill.kt @@ -5,11 +5,8 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.material.Checkbox import androidx.compose.material.CheckboxDefaults import androidx.compose.material.MaterialTheme @@ -21,10 +18,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import com.pixelized.desktop.lwa.ui.composable.circle.MasteryShape +import com.pixelized.desktop.lwa.ui.composable.shapes.MasteryShape import com.pixelized.desktop.lwa.ui.composable.tooltip.TooltipLayout import com.pixelized.desktop.lwa.ui.composable.tooltip.TooltipUio -import com.pixelized.desktop.lwa.ui.screen.roll.RollActionUio +import com.pixelized.desktop.lwa.ui.overlay.roll.RollAction.RollActionUio @Stable data class CharacterDetailSheetSkillUio( diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/PlayerPortrait.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/PlayerPortrait.kt index dc729ba..434cc15 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/PlayerPortrait.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/PlayerPortrait.kt @@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Icon +import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable @@ -39,6 +40,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage +import com.pixelized.desktop.lwa.ui.composable.shapes.ArrowShape import com.pixelized.desktop.lwa.ui.theme.lwa import com.pixelized.shared.lwa.model.campaign.Campaign import lwacharactersheet.composeapp.generated.resources.Res @@ -57,6 +59,7 @@ data class PlayerPortraitUio( val maxHp: Int, val pp: Int, val maxPp: Int, + val levelUp: Boolean, ) object PlayerPortrait { @@ -72,6 +75,7 @@ fun PlayerPortrait( bloodColor: Color = PlayerPortrait.Default.bloodColor, character: PlayerPortraitUio, onCharacter: (id: Campaign.CharacterInstance.Id) -> Unit, + onLevelUp: (id: Campaign.CharacterInstance.Id) -> Unit, ) { val colorScheme = MaterialTheme.lwa.colorScheme @@ -102,6 +106,15 @@ fun PlayerPortrait( hp = character.hp.toFloat(), ) + if (character.levelUp) { + IconButton( + modifier = Modifier.offset(x = (-8).dp, y = (-8).dp), + onClick = { onLevelUp(character.id) }, + ) { + ArrowShape() + } + } + Column( modifier = Modifier .fillMaxSize() diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/PlayerRibbon.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/PlayerRibbon.kt index 7a77c5f..523c14c 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/PlayerRibbon.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/PlayerRibbon.kt @@ -25,6 +25,7 @@ fun PlayerRibbon( playerRibbonViewModel: PlayerRibbonViewModel = koinViewModel(), padding: PaddingValues = PaddingValues(all = 8.dp), onCharacter: (id: Campaign.CharacterInstance.Id) -> Unit, + onLevelUp: (id: Campaign.CharacterInstance.Id) -> Unit, ) { val characters = playerRibbonViewModel.characters.collectAsState() @@ -42,6 +43,7 @@ fun PlayerRibbon( size = PlayerRibbon.Default.size, character = it, onCharacter = onCharacter, + onLevelUp = onLevelUp, ) PlayerPortraitRoll( size = PlayerRibbon.Default.size, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/PlayerRibbonFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/PlayerRibbonFactory.kt index 11098b7..d4fb5b4 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/PlayerRibbonFactory.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/PlayerRibbonFactory.kt @@ -31,6 +31,7 @@ class PlayerRibbonFactory( maxHp = alteredCharacterSheet.maxHp, pp = alteredCharacterSheet.maxPp - characterInstance.power, maxPp = alteredCharacterSheet.maxPp, + levelUp = alteredCharacterSheet.shouldLevelUp, ) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/PlayerRibbonViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/PlayerRibbonViewModel.kt index 77705cf..31636d1 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/PlayerRibbonViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/PlayerRibbonViewModel.kt @@ -16,11 +16,9 @@ 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.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapMerge import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch import java.text.Collator class PlayerRibbonViewModel( @@ -38,8 +36,8 @@ class PlayerRibbonViewModel( combine>( flows = campaign.characters.map { entry -> combine( - characterRepository.characterDetailFlow(characterId = entry.key.characterSheetId), - alterationRepository.alterationsFlow(characterId = entry.key), + characterRepository.characterDetailFlow(characterSheetId = entry.key.characterSheetId), + alterationRepository.alterationsFlow(characterInstanceId = entry.key), ) { sheet, alterations -> ribbonFactory.convertToPlayerPortraitUio( characterSheet = sheet, @@ -71,7 +69,7 @@ class PlayerRibbonViewModel( LaunchedEffect(characterSheetId) { rollHistoryRepository.rolls.collect { roll -> if (settingsRepository.settings().dynamicDice) { - if (roll.characterId == characterSheetId) { + if (roll.characterSheetId == characterSheetId) { state.value = PlayerPortraitRollUio( characterId = characterSheetId, value = roll.rollValue, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/toolbar/CampaignToolbar.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/toolbar/CampaignToolbar.kt index 809d2d9..6a45ed9 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/toolbar/CampaignToolbar.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/toolbar/CampaignToolbar.kt @@ -58,7 +58,7 @@ fun CampaignToolbar( isOverflowMenuOpen = isOverflowMenuOpen, networkMenu = { NetworkPage( - modifier = Modifier.size(384.dp, 240.dp), + modifier = Modifier.size(384.dp + 96.dp, 240.dp), viewModel = networkViewModel ) }, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/characterSheet/detail/CharacterSheetFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/characterSheet/detail/CharacterSheetFactory.kt index 7a150b9..5b32fda 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/characterSheet/detail/CharacterSheetFactory.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/characterSheet/detail/CharacterSheetFactory.kt @@ -1,10 +1,9 @@ package com.pixelized.desktop.lwa.ui.screen.characterSheet.detail import com.pixelized.desktop.lwa.ui.composable.tooltip.TooltipUio -import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.CharacterDetailSheetSkillUio import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.CharacterSheetPageUio.Characteristic import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.CharacterSheetPageUio.Node -import com.pixelized.desktop.lwa.ui.screen.roll.RollActionUio +import com.pixelized.desktop.lwa.ui.overlay.roll.RollAction.RollActionUio import com.pixelized.shared.lwa.model.AlteredCharacterSheetFactory import com.pixelized.shared.lwa.model.alteration.FieldAlteration import com.pixelized.shared.lwa.model.campaign.Campaign @@ -51,133 +50,133 @@ class CharacterSheetFactory( ) { suspend fun convertToUio( characterSheet: CharacterSheet?, - instanceId: Campaign.CharacterInstance.Id, + characterInstanceId: Campaign.CharacterInstance.Id, campaign: Campaign, alterations: Map>, ): CharacterSheetPageUio? { if (characterSheet == null) return null - val alteredSheet = alteredCharacterSheetFactory.sheet( + val alteredCharacterSheet = alteredCharacterSheetFactory.sheet( characterSheet = characterSheet, alterations = alterations, ) - val instance = campaign.character(id = instanceId) + val instance = campaign.character(id = characterInstanceId) return CharacterSheetPageUio( - id = alteredSheet.id, - name = alteredSheet.name, + id = alteredCharacterSheet.id, + name = alteredCharacterSheet.name, characteristics = listOf( Characteristic( id = CharacteristicId.STR, label = getString(Res.string.character_sheet__characteristics__str), - value = "${alteredSheet.strength}", + value = "${alteredCharacterSheet.strength}", editable = false, tooltips = TooltipUio( title = getString(Res.string.character_sheet__characteristics__str), description = getString(Res.string.tooltip__characteristics__strength), ), roll = RollActionUio( - characterSheetId = alteredSheet.id, + characterInstanceId = characterInstanceId, label = getString(Res.string.character_sheet__characteristics__str), rollAction = "1d100", - rollSuccessValue = alteredSheet.strength * 5 - instance.diminished, + rollSuccessValue = alteredCharacterSheet.strength * 5 - instance.diminished, ), ), Characteristic( id = CharacteristicId.DEX, label = getString(Res.string.character_sheet__characteristics__dex), - value = "${alteredSheet.dexterity}", + value = "${alteredCharacterSheet.dexterity}", editable = false, tooltips = TooltipUio( title = getString(Res.string.character_sheet__characteristics__dex), description = getString(Res.string.tooltip__characteristics__dexterity), ), roll = RollActionUio( - characterSheetId = alteredSheet.id, + characterInstanceId = characterInstanceId, label = getString(Res.string.character_sheet__characteristics__dex), rollAction = "1d100", - rollSuccessValue = alteredSheet.dexterity * 5 - instance.diminished, + rollSuccessValue = alteredCharacterSheet.dexterity * 5 - instance.diminished, ), ), Characteristic( id = CharacteristicId.CON, label = getString(Res.string.character_sheet__characteristics__con), - value = "${alteredSheet.constitution}", + value = "${alteredCharacterSheet.constitution}", editable = false, tooltips = TooltipUio( title = getString(Res.string.character_sheet__characteristics__con), description = getString(Res.string.tooltip__characteristics__constitution), ), roll = RollActionUio( - characterSheetId = alteredSheet.id, + characterInstanceId = characterInstanceId, label = getString(Res.string.character_sheet__characteristics__con), rollAction = "1d100", - rollSuccessValue = alteredSheet.constitution * 5 - instance.diminished, + rollSuccessValue = alteredCharacterSheet.constitution * 5 - instance.diminished, ), ), Characteristic( id = CharacteristicId.HEI, label = getString(Res.string.character_sheet__characteristics__hei), - value = "${alteredSheet.height}", + value = "${alteredCharacterSheet.height}", editable = false, tooltips = TooltipUio( title = getString(Res.string.character_sheet__characteristics__hei), description = getString(Res.string.tooltip__characteristics__height), ), roll = RollActionUio( - characterSheetId = alteredSheet.id, + characterInstanceId = characterInstanceId, label = getString(Res.string.character_sheet__characteristics__hei), rollAction = "1d100", - rollSuccessValue = alteredSheet.height * 5 - instance.diminished, + rollSuccessValue = alteredCharacterSheet.height * 5 - instance.diminished, ), ), Characteristic( id = CharacteristicId.INT, label = getString(Res.string.character_sheet__characteristics__int), - value = "${alteredSheet.intelligence}", + value = "${alteredCharacterSheet.intelligence}", editable = false, tooltips = TooltipUio( title = getString(Res.string.character_sheet__characteristics__int), description = getString(Res.string.tooltip__characteristics__intelligence), ), roll = RollActionUio( - characterSheetId = alteredSheet.id, + characterInstanceId = characterInstanceId, label = getString(Res.string.character_sheet__characteristics__int), rollAction = "1d100", - rollSuccessValue = alteredSheet.intelligence * 5 - instance.diminished, + rollSuccessValue = alteredCharacterSheet.intelligence * 5 - instance.diminished, ), ), Characteristic( id = CharacteristicId.POW, label = getString(Res.string.character_sheet__characteristics__pow), - value = "${alteredSheet.power}", + value = "${alteredCharacterSheet.power}", editable = false, tooltips = TooltipUio( title = getString(Res.string.character_sheet__characteristics__pow), description = getString(Res.string.tooltip__characteristics__power), ), roll = RollActionUio( - characterSheetId = alteredSheet.id, + characterInstanceId = characterInstanceId, label = getString(Res.string.character_sheet__characteristics__pow), rollAction = "1d100", - rollSuccessValue = alteredSheet.power * 5 - instance.diminished, + rollSuccessValue = alteredCharacterSheet.power * 5 - instance.diminished, ), ), Characteristic( id = CharacteristicId.CHA, label = getString(Res.string.character_sheet__characteristics__cha), - value = "${alteredSheet.charisma}", + value = "${alteredCharacterSheet.charisma}", editable = false, tooltips = TooltipUio( title = getString(Res.string.character_sheet__characteristics__cha), description = getString(Res.string.tooltip__characteristics__charisma), ), roll = RollActionUio( - characterSheetId = alteredSheet.id, + characterInstanceId = characterInstanceId, label = getString(Res.string.character_sheet__characteristics__cha), rollAction = "1d100", - rollSuccessValue = alteredSheet.charisma * 5 - instance.diminished, + rollSuccessValue = alteredCharacterSheet.charisma * 5 - instance.diminished, ), ), ), @@ -185,7 +184,7 @@ class CharacterSheetFactory( Characteristic( id = CharacteristicId.MOV, label = getString(Res.string.character_sheet__sub_characteristics__movement), - value = "${alteredSheet.movement}", + value = "${alteredCharacterSheet.movement}", editable = false, tooltips = TooltipUio( title = getString(Res.string.character_sheet__sub_characteristics__movement), @@ -196,7 +195,7 @@ class CharacterSheetFactory( Characteristic( id = CharacteristicId.HP, label = getString(Res.string.character_sheet__sub_characteristics__hit_point), - value = alteredSheet.maxHp.let { maxHp -> "${maxHp - instance.damage}/${maxHp}" }, + value = alteredCharacterSheet.maxHp.let { maxHp -> "${maxHp - instance.damage}/${maxHp}" }, editable = true, tooltips = TooltipUio( title = getString(Res.string.character_sheet__sub_characteristics__hit_point), @@ -207,7 +206,7 @@ class CharacterSheetFactory( Characteristic( id = CharacteristicId.PP, label = getString(Res.string.character_sheet__sub_characteristics__power_point), - value = alteredSheet.maxPp.let { maxPp -> "${maxPp - instance.power}/${maxPp}" }, + value = alteredCharacterSheet.maxPp.let { maxPp -> "${maxPp - instance.power}/${maxPp}" }, editable = true, tooltips = TooltipUio( title = getString(Res.string.character_sheet__sub_characteristics__power_point), @@ -218,7 +217,7 @@ class CharacterSheetFactory( Characteristic( id = CharacteristicId.DMG, label = getString(Res.string.character_sheet__sub_characteristics__damage_bonus), - value = alteredSheet.damageBonus, + value = alteredCharacterSheet.damageBonus, editable = false, tooltips = TooltipUio( title = getString(Res.string.character_sheet__sub_characteristics__damage_bonus), @@ -229,7 +228,7 @@ class CharacterSheetFactory( Characteristic( id = CharacteristicId.ARMOR, label = getString(Res.string.character_sheet__sub_characteristics__armor), - value = "${alteredSheet.armor}", + value = "${alteredCharacterSheet.armor}", editable = false, tooltips = TooltipUio( title = getString(Res.string.character_sheet__sub_characteristics__armor), @@ -240,7 +239,7 @@ class CharacterSheetFactory( Characteristic( id = CharacteristicId.LB, label = getString(Res.string.character_sheet__sub_characteristics__learning), - value = "${alteredSheet.learning}", + value = "${alteredCharacterSheet.learning}", editable = false, tooltips = TooltipUio( title = getString(Res.string.character_sheet__sub_characteristics__learning), @@ -251,7 +250,7 @@ class CharacterSheetFactory( Characteristic( id = CharacteristicId.GHP, label = getString(Res.string.character_sheet__sub_characteristics__hp_grow), - value = "${alteredSheet.hpGrow}", + value = "${alteredCharacterSheet.hpGrow}", editable = false, tooltips = TooltipUio( title = getString(Res.string.character_sheet__sub_characteristics__hp_grow), @@ -262,7 +261,7 @@ class CharacterSheetFactory( ), commonSkills = characterSheet.commonSkills.map { skill -> val value = skillUseCase.computeSkillValue( - sheet = characterSheet, + sheet = alteredCharacterSheet, skill = skill, diminished = instance.diminished, alterations = alterations, @@ -279,7 +278,7 @@ class CharacterSheetFactory( ) }, roll = RollActionUio( - characterSheetId = alteredSheet.id, + characterInstanceId = characterInstanceId, label = skill.label, rollAction = "1d100", rollSuccessValue = value, @@ -288,7 +287,7 @@ class CharacterSheetFactory( }, specialSKills = characterSheet.specialSkills.map { skill -> val value = skillUseCase.computeSkillValue( - sheet = characterSheet, + sheet = alteredCharacterSheet, skill = skill, diminished = instance.diminished, alterations = alterations, @@ -305,7 +304,7 @@ class CharacterSheetFactory( ) }, roll = RollActionUio( - characterSheetId = alteredSheet.id, + characterInstanceId = characterInstanceId, label = skill.label, rollAction = "1d100", rollSuccessValue = value, @@ -314,7 +313,7 @@ class CharacterSheetFactory( }, magicsSkills = characterSheet.magicSkills.map { skill -> val value = skillUseCase.computeSkillValue( - sheet = characterSheet, + sheet = alteredCharacterSheet, skill = skill, diminished = instance.diminished, alterations = alterations, @@ -331,7 +330,7 @@ class CharacterSheetFactory( ) }, roll = RollActionUio( - characterSheetId = alteredSheet.id, + characterInstanceId = characterInstanceId, label = skill.label, rollAction = "1d100", rollSuccessValue = value, @@ -344,7 +343,7 @@ class CharacterSheetFactory( label = it.label, value = it.roll, roll = RollActionUio( - characterSheetId = alteredSheet.id, + characterInstanceId = characterInstanceId, label = it.label, rollAction = it.roll, rollSuccessValue = null, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/characterSheet/detail/CharacterSheetPage.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/characterSheet/detail/CharacterSheetPage.kt index 4e94b05..2f6fa76 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/characterSheet/detail/CharacterSheetPage.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/characterSheet/detail/CharacterSheetPage.kt @@ -68,9 +68,9 @@ import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.CharacterSheetP import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.dialog.CharacterSheetDeleteConfirmationDialog import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.dialog.DiminishedStatDialog import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.preview.rememberCharacterSheetPreview -import com.pixelized.desktop.lwa.ui.screen.roll.RollActionUio -import com.pixelized.desktop.lwa.ui.screen.roll.RollPage -import com.pixelized.desktop.lwa.ui.screen.roll.RollViewModel +import com.pixelized.desktop.lwa.ui.overlay.roll.RollAction.RollActionUio +import com.pixelized.desktop.lwa.ui.overlay.roll.RollPage +import com.pixelized.desktop.lwa.ui.overlay.roll.RollViewModel import com.pixelized.desktop.lwa.utils.preview.ContentPreview import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -169,9 +169,11 @@ fun CharacterSheetPage( }, onCharacteristic = { characteristic -> if (characteristic.roll == null) return@CharacterSheetPageContent - rollViewModel.prepareRoll(characteristic.roll) - blurController.show() - viewModel.showRollOverlay() + scope.launch { + rollViewModel.prepareRoll(characteristic.roll) + blurController.show() + viewModel.showRollOverlay() + } }, onSubCharacteristic = { // blurController.show() @@ -180,15 +182,19 @@ fun CharacterSheetPage( // } }, onSkill = { node -> - blurController.show() - rollViewModel.prepareRoll(node.roll) - viewModel.showRollOverlay() + scope.launch { + blurController.show() + rollViewModel.prepareRoll(node.roll) + viewModel.showRollOverlay() + } }, onUseSkill = viewModel::onUseSkill, onRoll = { roll -> - blurController.show() - rollViewModel.prepareRoll(roll.roll) - viewModel.showRollOverlay() + scope.launch { + blurController.show() + rollViewModel.prepareRoll(roll.roll) + viewModel.showRollOverlay() + } }, ) } @@ -210,6 +216,11 @@ fun CharacterSheetPage( blurController.hide() viewModel.hideRollOverlay() }, + onRoll = { + scope.launch { + rollViewModel.roll() + } + } ) else -> Box( diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/characterSheet/detail/CharacterSheetViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/characterSheet/detail/CharacterSheetViewModel.kt index 0651d73..1a093ff 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/characterSheet/detail/CharacterSheetViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/characterSheet/detail/CharacterSheetViewModel.kt @@ -56,15 +56,15 @@ class CharacterSheetViewModel( .stateIn(scope = viewModelScope, SharingStarted.Lazily, null) val sheetFlow = combine( - characterRepository.characterDetailFlow(characterId = argument.characterInstanceId.characterSheetId), + characterRepository.characterDetailFlow(characterSheetId = argument.characterInstanceId.characterSheetId), campaignRepository.campaignFlow, - alteration.alterationsFlow(characterId = argument.characterInstanceId), + alteration.alterationsFlow(characterInstanceId = argument.characterInstanceId), transform = { sheet, campaign, alterations -> factory.convertToUio( characterSheet = sheet, - instanceId = argument.characterInstanceId, + characterInstanceId = argument.characterInstanceId, campaign = campaign, - alterations = alterations + alterations = alterations, ) }, ).stateIn( diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/characterSheet/edit/CharacterSheetEditFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/characterSheet/edit/CharacterSheetEditFactory.kt index ab45c60..ac12afc 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/characterSheet/edit/CharacterSheetEditFactory.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/characterSheet/edit/CharacterSheetEditFactory.kt @@ -83,6 +83,7 @@ class CharacterSheetEditFactory( portrait = currentSheet?.portrait, thumbnail = currentSheet?.thumbnail, level = level, + shouldLevelUp = currentSheet?.shouldLevelUp ?: false, strength = strength, dexterity = dexterity, constitution = constitution, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/levelup/LevelScreen.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/levelup/LevelScreen.kt new file mode 100644 index 0000000..0b76732 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/levelup/LevelScreen.kt @@ -0,0 +1,384 @@ +package com.pixelized.desktop.lwa.ui.screen.levelup + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.draggable +import androidx.compose.foundation.gestures.rememberDraggableState +import androidx.compose.foundation.gestures.scrollBy +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.material.TopAppBar +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.State +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.FilterQuality +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.type +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.navigation.NavHostController +import coil3.compose.AsyncImage +import com.pixelized.desktop.lwa.LocalRollHostState +import com.pixelized.desktop.lwa.ui.composable.decoratedBox.DecoratedBox +import com.pixelized.desktop.lwa.ui.composable.key.KeyHandler +import com.pixelized.desktop.lwa.ui.navigation.screen.LocalScreenController +import com.pixelized.desktop.lwa.ui.navigation.screen.destination.LevelUpDestination +import com.pixelized.desktop.lwa.ui.overlay.roll.RollResult +import com.pixelized.desktop.lwa.ui.screen.levelup.skill.LevelUpCharacteristic +import com.pixelized.desktop.lwa.ui.screen.levelup.skill.LevelUpCharacteristicUio +import com.pixelized.desktop.lwa.ui.screen.levelup.skill.LevelUpSkill +import com.pixelized.desktop.lwa.ui.screen.levelup.skill.LevelUpSkillUio +import com.pixelized.desktop.lwa.ui.theme.lwa +import kotlinx.coroutines.launch +import lwacharactersheet.composeapp.generated.resources.Res +import lwacharactersheet.composeapp.generated.resources.character_sheet__skills__common_title +import lwacharactersheet.composeapp.generated.resources.character_sheet__skills__magic_title +import lwacharactersheet.composeapp.generated.resources.character_sheet__skills__special_title +import lwacharactersheet.composeapp.generated.resources.level_up__action +import lwacharactersheet.composeapp.generated.resources.level_up__character_level_description +import lwacharactersheet.composeapp.generated.resources.level_up__title +import org.jetbrains.compose.resources.StringResource +import org.jetbrains.compose.resources.stringResource +import org.koin.compose.viewmodel.koinViewModel + +@Stable +data class LevelUpHeaderUio( + val name: String, + val level: Int, + val portrait: String?, +) + +@Stable +data class LevelUpSectionUio( + val commonSkills: List?, + val specialSkills: List?, + val magicSkills: List?, +) + +@Composable +fun LevelUpScreen( + viewModel: LevelUpViewModel = koinViewModel(), +) { + val screen = LocalScreenController.current + val roll = LocalRollHostState.current + val scope = rememberCoroutineScope() + + val header = viewModel.header.collectAsState() + val characteristics = viewModel.characteristics.collectAsState() + val skills = viewModel.skills.collectAsState() + val commitLevelUp = viewModel.displayCommitLevelUp.collectAsState() + + Surface { + LevelUpContent( + modifier = Modifier.fillMaxSize(), + commitLevelUp = commitLevelUp, + header = header, + characteristics = characteristics, + skills = skills, + onBack = { + screen.navigateBack() + }, + onCommitLevelUp = { + scope.launch { + viewModel.commitLevelUp() + screen.navigateBack() + } + }, + onCharacteristic = { + viewModel.selectCharacteristic(it.characteristicId) + }, + onSkill = { + scope.launch { + if (it.roll != null) { + val result: RollResult = roll.showRollOverlay(roll = it.roll) + viewModel.applyRollResult(skillId = it.skillId, result = result) + } + } + }, + ) + } + + LevelUpKeyHandler( + onDismissRequest = { + screen.navigateBack() + } + ) +} + +@Composable +private fun LevelUpContent( + modifier: Modifier = Modifier, + scrollState: ScrollState = rememberScrollState(), + lazyListState: LazyListState = rememberLazyListState(), + commitLevelUp: State, + header: State, + characteristics: State>, + skills: State, + onBack: () -> Unit, + onCommitLevelUp: () -> Unit, + onCharacteristic: (LevelUpCharacteristicUio) -> Unit, + onSkill: (LevelUpSkillUio) -> Unit, +) { + val scope = rememberCoroutineScope() + + LevelUpLayout( + modifier = modifier, + topBar = { + TopAppBar( + title = { + Text(text = stringResource(Res.string.level_up__title)) + }, + navigationIcon = { + IconButton( + onClick = onBack, + ) { + Icon( + imageVector = Icons.AutoMirrored.Default.ArrowBack, + contentDescription = null, + ) + } + }, + actions = { + AnimatedVisibility( + visible = commitLevelUp.value, + enter = fadeIn(), + exit = fadeOut(), + ) { + TextButton( + onClick = onCommitLevelUp, + ) { + Text(text = stringResource(Res.string.level_up__action)) + } + } + }, + ) + }, + background = { + AsyncImage( + modifier = Modifier.matchParentSize(), + model = header.value?.portrait, + contentScale = ContentScale.FillHeight, + alignment = Alignment.Center, + filterQuality = FilterQuality.High, + contentDescription = null, + ) + }, + panel = { + Column( + modifier = Modifier + .matchParentSize() + .background(color = MaterialTheme.lwa.colorScheme.elevated.base1dp), + verticalArrangement = Arrangement.spacedBy(space = 8.dp), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .background(color = MaterialTheme.lwa.colorScheme.elevated.base2dp) + .padding(horizontal = 16.dp, vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(space = 8.dp), + ) { + Text( + style = MaterialTheme.lwa.typography.base.h5, + text = header.value?.name ?: "" + ) + Text( + style = MaterialTheme.lwa.typography.base.body1, + text = (header.value?.level ?: 0).let { + stringResource(Res.string.level_up__character_level_description, it, it + 1) + }, + ) + } + Column( + modifier = Modifier.verticalScroll(state = scrollState), + ) { + LazyRow( + modifier = Modifier.draggable( + orientation = Orientation.Horizontal, + state = rememberDraggableState { delta -> + scope.launch { + lazyListState.scrollBy(-delta) + } + }, + ), + state = lazyListState, + contentPadding = PaddingValues(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(space = 8.dp), + ) { + items( + items = characteristics.value, + key = { it.characteristicId } + ) { + LevelUpCharacteristic( + modifier = Modifier.size(size = MaterialTheme.lwa.size.characteristic), + characteristic = it, + onClick = { onCharacteristic(it) }, + ) + } + } + Column( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(space = 8.dp), + ) { + skills.value?.commonSkills?.let { skills -> + SubSkillSection( + modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp), + label = Res.string.character_sheet__skills__common_title, + skills = skills, + onSkill = onSkill, + ) + } + skills.value?.specialSkills?.let { skills -> + SubSkillSection( + modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp), + label = Res.string.character_sheet__skills__special_title, + skills = skills, + onSkill = onSkill, + ) + } + skills.value?.magicSkills?.let { skills -> + SubSkillSection( + modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp), + label = Res.string.character_sheet__skills__magic_title, + skills = skills, + onSkill = onSkill, + ) + } + } + } + } + }, + ) +} + +@Composable +private fun LevelUpLayout( + modifier: Modifier, + panelShape: Shape = MaterialTheme.lwa.shapes.panel, + panelColor: Color = MaterialTheme.lwa.colorScheme.elevated.base1dp, + topBar: @Composable () -> Unit, + background: @Composable BoxScope.() -> Unit, + panel: @Composable BoxScope.() -> Unit, +) { + Scaffold( + modifier = modifier, + topBar = topBar, + content = { paddingValues -> + Box( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + Box( + modifier = Modifier + .matchParentSize() + .offset(x = 128.dp * -2) + ) { + background() + } + Box( + modifier = Modifier + .align(Alignment.CenterEnd) + .padding(all = 8.dp) + .fillMaxHeight() + .width(width = 128.dp * 4) + .clip(shape = panelShape) + .background(color = panelColor, shape = panelShape) + ) { + panel() + } + } + } + ) +} + +@Composable +private fun SubSkillSection( + modifier: Modifier = Modifier, + paddingValues: PaddingValues = PaddingValues(bottom = 8.dp, top = 4.dp), + label: StringResource, + skills: List, + onSkill: (LevelUpSkillUio) -> Unit, +) { + DecoratedBox( + modifier = modifier, + ) { + Column { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(paddingValues), + style = MaterialTheme.typography.caption, + textAlign = TextAlign.Center, + text = stringResource(label), + ) + skills.forEach { skill -> + LevelUpSkill( + modifier = Modifier.height(height = 32.dp), + skill = skill, + onSkill = onSkill, + ) + } + } + } +} + +@Composable +private fun LevelUpKeyHandler( + onDismissRequest: () -> Unit, +) { + KeyHandler { + when { + it.type == KeyEventType.KeyUp && it.key == Key.Escape -> { + onDismissRequest() + true + } + + else -> false + } + } +} + +private fun NavHostController.navigateBack() = popBackStack( + route = LevelUpDestination.baseRoute(), + inclusive = true, +) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/levelup/LevelUpFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/levelup/LevelUpFactory.kt new file mode 100644 index 0000000..0c9a032 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/levelup/LevelUpFactory.kt @@ -0,0 +1,225 @@ +package com.pixelized.desktop.lwa.ui.screen.levelup + +import com.pixelized.desktop.lwa.ui.composable.tooltip.TooltipUio +import com.pixelized.desktop.lwa.ui.overlay.roll.RollAction.RollActionUio +import com.pixelized.desktop.lwa.ui.overlay.roll.RollResult +import com.pixelized.desktop.lwa.ui.screen.levelup.skill.LevelUpCharacteristicUio +import com.pixelized.desktop.lwa.ui.screen.levelup.skill.LevelUpSkillUio +import com.pixelized.shared.lwa.model.AlteredCharacterSheet +import com.pixelized.shared.lwa.model.AlteredCharacterSheetFactory +import com.pixelized.shared.lwa.model.campaign.Campaign +import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet +import com.pixelized.shared.lwa.usecase.ExpressionUseCase +import lwacharactersheet.composeapp.generated.resources.Res +import lwacharactersheet.composeapp.generated.resources.character_sheet__characteristics__cha +import lwacharactersheet.composeapp.generated.resources.character_sheet__characteristics__con +import lwacharactersheet.composeapp.generated.resources.character_sheet__characteristics__dex +import lwacharactersheet.composeapp.generated.resources.character_sheet__characteristics__hei +import lwacharactersheet.composeapp.generated.resources.character_sheet__characteristics__int +import lwacharactersheet.composeapp.generated.resources.character_sheet__characteristics__pow +import lwacharactersheet.composeapp.generated.resources.character_sheet__characteristics__str +import lwacharactersheet.composeapp.generated.resources.tooltip__characteristics__charisma +import lwacharactersheet.composeapp.generated.resources.tooltip__characteristics__constitution +import lwacharactersheet.composeapp.generated.resources.tooltip__characteristics__dexterity +import lwacharactersheet.composeapp.generated.resources.tooltip__characteristics__height +import lwacharactersheet.composeapp.generated.resources.tooltip__characteristics__intelligence +import lwacharactersheet.composeapp.generated.resources.tooltip__characteristics__power +import lwacharactersheet.composeapp.generated.resources.tooltip__characteristics__strength +import org.jetbrains.compose.resources.getString + +class LevelUpFactory( + private val alteredCharacterSheetFactory: AlteredCharacterSheetFactory, + private val expressionUseCase: ExpressionUseCase, +) { + + fun convertToLevelUpHeaderUio( + characterSheet: CharacterSheet?, + ): LevelUpHeaderUio? { + if (characterSheet == null) return null + return LevelUpHeaderUio( + name = characterSheet.name, + portrait = characterSheet.portrait, + level = characterSheet.level, + ) + } + + suspend fun convertToLevelUpCharacteristicUio( + characterSheet: CharacterSheet?, + selectedCharacteristicId: String?, + ): List { + if (characterSheet == null) return emptyList() + + val alteredCharacterSheet = alteredCharacterSheetFactory.sheet( + characterSheet = characterSheet, + alterations = emptyMap(), + ) + + return listOf( + LevelUpCharacteristicUio( + characteristicId = CharacterSheet.CharacteristicId.STR, + label = getString(Res.string.character_sheet__characteristics__str), + value = alteredCharacterSheet.strength, + selected = selectedCharacteristicId == CharacterSheet.CharacteristicId.STR, + tooltips = TooltipUio( + title = getString(Res.string.character_sheet__characteristics__str), + description = getString(Res.string.tooltip__characteristics__strength), + ), + ), + LevelUpCharacteristicUio( + characteristicId = CharacterSheet.CharacteristicId.DEX, + label = getString(Res.string.character_sheet__characteristics__dex), + value = alteredCharacterSheet.dexterity, + selected = selectedCharacteristicId == CharacterSheet.CharacteristicId.DEX, + tooltips = TooltipUio( + title = getString(Res.string.character_sheet__characteristics__dex), + description = getString(Res.string.tooltip__characteristics__dexterity), + ), + ), + LevelUpCharacteristicUio( + characteristicId = CharacterSheet.CharacteristicId.CON, + label = getString(Res.string.character_sheet__characteristics__con), + value = alteredCharacterSheet.constitution, + selected = selectedCharacteristicId == CharacterSheet.CharacteristicId.CON, + tooltips = TooltipUio( + title = getString(Res.string.character_sheet__characteristics__con), + description = getString(Res.string.tooltip__characteristics__constitution), + ), + ), + LevelUpCharacteristicUio( + characteristicId = CharacterSheet.CharacteristicId.HEI, + label = getString(Res.string.character_sheet__characteristics__hei), + value = alteredCharacterSheet.height, + selected = selectedCharacteristicId == CharacterSheet.CharacteristicId.HEI, + tooltips = TooltipUio( + title = getString(Res.string.character_sheet__characteristics__hei), + description = getString(Res.string.tooltip__characteristics__height), + ), + ), + LevelUpCharacteristicUio( + characteristicId = CharacterSheet.CharacteristicId.INT, + label = getString(Res.string.character_sheet__characteristics__int), + value = alteredCharacterSheet.intelligence, + selected = selectedCharacteristicId == CharacterSheet.CharacteristicId.INT, + tooltips = TooltipUio( + title = getString(Res.string.character_sheet__characteristics__int), + description = getString(Res.string.tooltip__characteristics__intelligence), + ), + ), + LevelUpCharacteristicUio( + characteristicId = CharacterSheet.CharacteristicId.POW, + label = getString(Res.string.character_sheet__characteristics__pow), + value = alteredCharacterSheet.power, + selected = selectedCharacteristicId == CharacterSheet.CharacteristicId.POW, + tooltips = TooltipUio( + title = getString(Res.string.character_sheet__characteristics__pow), + description = getString(Res.string.tooltip__characteristics__power), + ), + ), + LevelUpCharacteristicUio( + characteristicId = CharacterSheet.CharacteristicId.CHA, + label = getString(Res.string.character_sheet__characteristics__cha), + value = alteredCharacterSheet.charisma, + selected = selectedCharacteristicId == CharacterSheet.CharacteristicId.CHA, + tooltips = TooltipUio( + title = getString(Res.string.character_sheet__characteristics__cha), + description = getString(Res.string.tooltip__characteristics__charisma), + ), + ), + ) + } + + fun convertToLevelUpSectionUio( + characterInstanceId: Campaign.CharacterInstance.Id, + characterSheet: CharacterSheet?, + results: Map, + ): LevelUpSectionUio? { + if (characterSheet == null) return null + + val alteredCharacterSheet = alteredCharacterSheetFactory.sheet( + characterSheet = characterSheet, + alterations = emptyMap(), + ) + + val commonSkills = characterSheet.commonSkills.mapNotNull { skill -> + levelUpSkillUio( + characterInstanceId = characterInstanceId, + alteredCharacterSheet = alteredCharacterSheet, + skill = skill, + results = results, + ) + }.takeIf { + it.isNotEmpty() + } + + val specialSkills = characterSheet.specialSkills.mapNotNull { skill -> + levelUpSkillUio( + characterInstanceId = characterInstanceId, + alteredCharacterSheet = alteredCharacterSheet, + skill = skill, + results = results, + ) + }.takeIf { + it.isNotEmpty() + } + + val magicSkills = characterSheet.magicSkills.mapNotNull { skill -> + levelUpSkillUio( + characterInstanceId = characterInstanceId, + alteredCharacterSheet = alteredCharacterSheet, + skill = skill, + results = results, + ) + }.takeIf { + it.isNotEmpty() + } + + return LevelUpSectionUio( + commonSkills = commonSkills, + specialSkills = specialSkills, + magicSkills = magicSkills, + ) + } + + private fun levelUpSkillUio( + characterInstanceId: Campaign.CharacterInstance.Id, + alteredCharacterSheet: AlteredCharacterSheet, + skill: CharacterSheet.Skill, + results: Map, + ): LevelUpSkillUio? { + if (!skill.used) return null + + val value = expressionUseCase.computeSkillValue( + sheet = alteredCharacterSheet, + skill = skill, + diminished = 0, + alterations = emptyMap(), + ) + + val success = (100 - value + alteredCharacterSheet.learning).coerceIn(1, 100) + + return LevelUpSkillUio( + skillId = skill.id, + label = skill.label, + value = value, + level = skill.level, + levelUp = results.isSkillLeveledUp(skillId = skill.id), + occupation = skill.occupation, + roll = when (results[skill.id]) { + null -> RollActionUio( + characterInstanceId = characterInstanceId, + label = skill.label, + rollAction = "1d100", + rollSuccessValue = success, + ) + + else -> null + }, + ) + } +} + +fun Map.isSkillLeveledUp(skillId: String): Boolean { + return this.getOrElse(skillId) { RollResult.Dismissed }.let { + it == RollResult.CriticalSuccess || it == RollResult.SpecialSuccess || it == RollResult.Success + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/levelup/LevelUpViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/levelup/LevelUpViewModel.kt new file mode 100644 index 0000000..1677700 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/levelup/LevelUpViewModel.kt @@ -0,0 +1,157 @@ +package com.pixelized.desktop.lwa.ui.screen.levelup + +import androidx.compose.material.SnackbarDuration +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository +import com.pixelized.desktop.lwa.ui.composable.error.ErrorSnackUio +import com.pixelized.desktop.lwa.ui.navigation.screen.destination.CharacterSheetDestination +import com.pixelized.desktop.lwa.ui.overlay.roll.RollResult +import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import lwacharactersheet.composeapp.generated.resources.Res +import lwacharactersheet.composeapp.generated.resources.error__default__action +import lwacharactersheet.composeapp.generated.resources.error__missing_character_sheet__label +import org.jetbrains.compose.resources.getString + +class LevelUpViewModel( + private val characterSheetRepository: CharacterSheetRepository, + private val levelUpFactory: LevelUpFactory, + savedStateHandle: SavedStateHandle, +) : ViewModel() { + private val argument = CharacterSheetDestination.Argument(savedStateHandle) + + private val _errors = MutableSharedFlow() + val error: SharedFlow = _errors + + private val results = MutableStateFlow>(emptyMap()) + private val selectedCharacteristicId = MutableStateFlow(null) + + val header: StateFlow = characterSheetRepository + .characterDetailFlow(characterSheetId = argument.characterInstanceId.characterSheetId) + .map(levelUpFactory::convertToLevelUpHeaderUio) + .stateIn( + scope = viewModelScope, + started = SharingStarted.Lazily, + initialValue = null, + ) + + val characteristics = combine( + characterSheetRepository.characterDetailFlow(characterSheetId = argument.characterInstanceId.characterSheetId), + selectedCharacteristicId, + levelUpFactory::convertToLevelUpCharacteristicUio, + ).stateIn( + scope = viewModelScope, + started = SharingStarted.Lazily, + initialValue = emptyList(), + ) + + val skills = combine( + characterSheetRepository.characterDetailFlow(characterSheetId = argument.characterInstanceId.characterSheetId), + results, + ) { characterSheet, results -> + levelUpFactory.convertToLevelUpSectionUio( + characterInstanceId = argument.characterInstanceId, + characterSheet = characterSheet, + results = results, + ) + }.stateIn( + scope = viewModelScope, + started = SharingStarted.Lazily, + initialValue = null, + ) + + val displayCommitLevelUp: StateFlow = combine( + skills, + results, + selectedCharacteristicId, + ) { skills, results, selectedCharacteristicId -> + val skillCount = skills?.let { + val magicSkillsCount = it.magicSkills?.size ?: 0 + val commonSkillsCount = it.commonSkills?.size ?: 0 + val specialSkillsCount = it.specialSkills?.size ?: 0 + magicSkillsCount + commonSkillsCount + specialSkillsCount + } + skillCount == results.size && selectedCharacteristicId != null + }.stateIn( + scope = viewModelScope, + started = SharingStarted.Lazily, + initialValue = false, + ) + + fun applyRollResult(skillId: String, result: RollResult) { + // Discard this case. + if (result == RollResult.Dismissed) return + // If not save the roll result to the map. + results.value = results.value.toMutableMap().also { + it[skillId] = result + } + } + + fun selectCharacteristic(characteristicId: String?) { + if (selectedCharacteristicId.value == characteristicId) { + selectedCharacteristicId.value = null + } else { + selectedCharacteristicId.value = characteristicId + } + } + + suspend fun commitLevelUp() { + val characterSheet = characterSheetRepository.characterDetail( + characterSheetId = argument.characterInstanceId.characterSheetId, + ) + + if (characterSheet == null) { + _errors.emit( + ErrorSnackUio( + message = getString(Res.string.error__missing_character_sheet__label), + action = getString(Res.string.error__default__action), + duration = SnackbarDuration.Long, + ) + ) + return + } + + val levelUpCharacter = characterSheet.copy( + level = characterSheet.level + 1, + shouldLevelUp = false, + strength = characterSheet.strength + if (selectedCharacteristicId.value == CharacteristicId.STR) 1 else 0, + dexterity = characterSheet.dexterity + if (selectedCharacteristicId.value == CharacteristicId.DEX) 1 else 0, + constitution = characterSheet.constitution + if (selectedCharacteristicId.value == CharacteristicId.CON) 1 else 0, + height = characterSheet.height + if (selectedCharacteristicId.value == CharacteristicId.HEI) 1 else 0, + intelligence = characterSheet.intelligence + if (selectedCharacteristicId.value == CharacteristicId.INT) 1 else 0, + power = characterSheet.power + if (selectedCharacteristicId.value == CharacteristicId.POW) 1 else 0, + charisma = characterSheet.charisma + if (selectedCharacteristicId.value == CharacteristicId.CHA) 1 else 0, + commonSkills = characterSheet.commonSkills.map { skill -> + skill.copy( + level = skill.level + if (results.value.isSkillLeveledUp(skillId = skill.id)) 1 else 0, + used = false, + ) + }, + specialSkills = characterSheet.specialSkills.map { skill -> + skill.copy( + level = skill.level + if (results.value.isSkillLeveledUp(skillId = skill.id)) 1 else 0, + used = false, + ) + }, + magicSkills = characterSheet.magicSkills.map { skill -> + skill.copy( + level = skill.level + if (results.value.isSkillLeveledUp(skillId = skill.id)) 1 else 0, + used = false, + ) + }, + ) + + characterSheetRepository.updateCharacter( + characterSheet = levelUpCharacter, + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/levelup/skill/LevelUpCharacteristic.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/levelup/skill/LevelUpCharacteristic.kt new file mode 100644 index 0000000..8750fa0 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/levelup/skill/LevelUpCharacteristic.kt @@ -0,0 +1,93 @@ +package com.pixelized.desktop.lwa.ui.screen.levelup.skill + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.SizeTransform +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.pixelized.desktop.lwa.ui.composable.decoratedBox.DecoratedBox +import com.pixelized.desktop.lwa.ui.composable.tooltip.TooltipLayout +import com.pixelized.desktop.lwa.ui.composable.tooltip.TooltipUio +import com.pixelized.desktop.lwa.ui.theme.lwa + +@Stable +data class LevelUpCharacteristicUio( + val characteristicId: String, + val value: Int, + val label: String, + val selected: Boolean, + val tooltips: TooltipUio, +) + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun LevelUpCharacteristic( + modifier: Modifier = Modifier, + paddingValues: PaddingValues = PaddingValues(all = 8.dp), + characteristic: LevelUpCharacteristicUio, + onClick: () -> Unit, +) { + TooltipLayout( + tooltip = characteristic.tooltips, + content = { + DecoratedBox( + modifier = Modifier + .clickable(onClick = onClick) + .padding(paddingValues = paddingValues) + .then(other = modifier), + ) { + Text( + modifier = Modifier.align(alignment = Alignment.TopCenter), + style = MaterialTheme.typography.caption, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + text = characteristic.label, + ) + AnimatedContent( + modifier = Modifier + .fillMaxWidth() + .align(alignment = Alignment.Center), + targetState = characteristic.selected, + transitionSpec = { + val enter = fadeIn() + slideInVertically { -32 } + val exit = fadeOut() + slideOutVertically { 32 } + enter togetherWith exit using SizeTransform(clip = false) + }, + ) { + Text( + modifier = Modifier.fillMaxWidth(), + style = MaterialTheme.typography.h3, + textAlign = TextAlign.Center, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + color = when (it) { + true -> MaterialTheme.lwa.colorScheme.base.secondary + else -> MaterialTheme.lwa.colorScheme.base.primary + }, + text = when (it) { + true -> "${characteristic.value + 1}" + else -> "${characteristic.value}" + } + ) + } + } + }, + ) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/levelup/skill/LevelUpSkill.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/levelup/skill/LevelUpSkill.kt new file mode 100644 index 0000000..2e650fc --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/levelup/skill/LevelUpSkill.kt @@ -0,0 +1,124 @@ +package com.pixelized.desktop.lwa.ui.screen.levelup.skill + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.SizeTransform +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.pixelized.desktop.lwa.ui.composable.shapes.MasteryShape +import com.pixelized.desktop.lwa.ui.overlay.roll.RollAction.RollActionUio +import com.pixelized.desktop.lwa.ui.theme.lwa +import lwacharactersheet.composeapp.generated.resources.Res +import lwacharactersheet.composeapp.generated.resources.level_up__skill_level +import org.jetbrains.compose.resources.stringResource + +@Stable +data class LevelUpSkillUio( + val skillId: String, + val label: String, + val value: Int, + val level: Int, + val levelUp: Boolean, + val occupation: Boolean, + val roll: RollActionUio?, +) + +@Composable +fun LevelUpSkill( + modifier: Modifier = Modifier, + paddingValues: PaddingValues = PaddingValues(start = 8.dp, end = 8.dp), + skill: LevelUpSkillUio, + onSkill: (LevelUpSkillUio) -> Unit, +) { + val valueColor = animateColorAsState( + targetValue = when (skill.roll) { + null -> MaterialTheme.lwa.colorScheme.base.onSurface + else -> MaterialTheme.lwa.colorScheme.base.primary + } + ) + Row( + modifier = Modifier + .let { + when (skill.roll) { + null -> it + else -> it.clickable(onClick = { onSkill(skill) }) + } + } + .padding(paddingValues = paddingValues) + .then(other = modifier), + horizontalArrangement = Arrangement.spacedBy(space = 8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + MasteryShape( + modifier = Modifier.padding(top = 4.dp), + multiplier = if (skill.occupation) 1 else 0, + ) + Row( + horizontalArrangement = Arrangement.spacedBy(space = 4.dp), + ) { + Text( + modifier = Modifier.alignByBaseline().weight(1f), + style = MaterialTheme.typography.body1, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + text = skill.label + ) + AnimatedContent( + targetState = skill.levelUp, + transitionSpec = { + val enter = fadeIn() + slideInVertically { -16 } + val exit = fadeOut() + slideOutVertically { 16 } + enter togetherWith exit using SizeTransform(clip = false) + } + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(space = 4.dp), + ) { + Text( + modifier = Modifier.alignByBaseline(), + style = MaterialTheme.typography.caption, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + text = stringResource( + resource = Res.string.level_up__skill_level, + when (it) { + true -> skill.level + 1 + else -> skill.level + }, + ) + ) + Text( + modifier = Modifier.alignByBaseline(), + style = MaterialTheme.typography.body1, + fontWeight = FontWeight.Bold, + color = when (it) { + true -> MaterialTheme.lwa.colorScheme.base.secondary + else -> valueColor.value + }, + text = when (it) { + true -> "${skill.value + 5}" + else -> "${skill.value}" + }, + ) + } + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/main/MainPageViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/main/MainPageViewModel.kt index 6fa5482..4101021 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/main/MainPageViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/main/MainPageViewModel.kt @@ -31,7 +31,7 @@ class MainPageViewModel( campaign.characters .map { entry -> characterSheetRepository - .characterDetailFlow(characterId = entry.key.characterSheetId) + .characterDetailFlow(characterSheetId = entry.key.characterSheetId) .map transform@{ sheet -> if (sheet == null) return@transform null CharacterUio(id = entry.key, name = sheet.name) @@ -57,7 +57,7 @@ class MainPageViewModel( campaign.npcs .map { entry -> characterSheetRepository - .characterDetailFlow(characterId = entry.key.characterSheetId) + .characterDetailFlow(characterSheetId = entry.key.characterSheetId) .map transform@{ sheet -> if (sheet == null) return@transform null CharacterUio(id = entry.key, name = sheet.name) diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/network/NetworkFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/network/NetworkFactory.kt index a4ec9a9..1c2f209 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/network/NetworkFactory.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/network/NetworkFactory.kt @@ -8,15 +8,19 @@ class NetworkFactory { player: String, status: Status, host: String, + resetHost: Boolean, port: Int, + resetPort: Boolean, ): NetworkPageUio { return NetworkPageUio( player = player, host = host, + resetHost = resetHost, port = "$port", + resetPort = resetPort, enableFields = status == Status.DISCONNECTED, enableActions = status == Status.DISCONNECTED && player.isNotBlank() && host.isNotBlank() && port > 0, - enableCancel = status == Status.CONNECTED + enableCancel = status == Status.CONNECTED, ) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/network/NetworkScreen.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/network/NetworkScreen.kt index c098b3f..01fa2dd 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/network/NetworkScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/network/NetworkScreen.kt @@ -1,6 +1,7 @@ package com.pixelized.desktop.lwa.ui.screen.network import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.togetherWith @@ -22,6 +23,7 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Icon import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme import androidx.compose.material.Scaffold import androidx.compose.material.SnackbarDuration import androidx.compose.material.Surface @@ -44,13 +46,16 @@ import com.pixelized.desktop.lwa.LocalSnackHost import com.pixelized.desktop.lwa.ui.composable.blur.BlurContent import com.pixelized.desktop.lwa.ui.composable.error.ErrorSnack import com.pixelized.desktop.lwa.ui.navigation.screen.LocalScreenController +import com.pixelized.desktop.lwa.ui.theme.lwa import lwacharactersheet.composeapp.generated.resources.Res +import lwacharactersheet.composeapp.generated.resources.ic_cancel_24dp import lwacharactersheet.composeapp.generated.resources.network__host__label import lwacharactersheet.composeapp.generated.resources.network__player_name__label import lwacharactersheet.composeapp.generated.resources.network__port__label import lwacharactersheet.composeapp.generated.resources.network__socket__connect_action import lwacharactersheet.composeapp.generated.resources.network__socket__disconnect_action import lwacharactersheet.composeapp.generated.resources.network__title +import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel @@ -58,7 +63,9 @@ import org.koin.compose.viewmodel.koinViewModel data class NetworkPageUio( val player: String, val host: String, + val resetHost: Boolean, val port: String, + val resetPort: Boolean, val enableFields: Boolean, val enableActions: Boolean, val enableCancel: Boolean, @@ -68,14 +75,18 @@ data class NetworkPageUio( fun empty( player: String = "", host: String = "", + resetHost: Boolean = false, port: String = "", + resetPort: Boolean = false, enableFields: Boolean = false, enableActions: Boolean = false, enableCancel: Boolean = false, ) = NetworkPageUio( player = player, host = host, + resetHost = resetHost, port = port, + resetPort = resetPort, enableFields = enableFields, enableActions = enableActions, enableCancel = enableCancel, @@ -128,10 +139,12 @@ fun NetworkScreen( NetworkContent( modifier = Modifier.fillMaxSize(), paddingValues = paddingValues, - player = viewModel.network.collectAsState(), + network = viewModel.network.collectAsState(), onPlayerChange = viewModel::onPlayerNameChange, onHostChange = viewModel::onHostChange, + onResetPortChange = viewModel::onResetPortChange, onPortChange = viewModel::onPortChange, + onResetHostChange = viewModel::onResetHostChange, onConnect = viewModel::connect, onDisconnect = viewModel::disconnect, ) @@ -182,10 +195,12 @@ fun NetworkPage( ) { NetworkContent( modifier = Modifier.fillMaxSize(), - player = viewModel.network.collectAsState(), + network = viewModel.network.collectAsState(), onPlayerChange = viewModel::onPlayerNameChange, onHostChange = viewModel::onHostChange, + onResetHostChange = viewModel::onResetHostChange, onPortChange = viewModel::onPortChange, + onResetPortChange = viewModel::onResetPortChange, onConnect = viewModel::connect, onDisconnect = viewModel::disconnect, ) @@ -222,10 +237,12 @@ private fun NetworkContent( modifier: Modifier = Modifier, scrollState: ScrollState = rememberScrollState(), paddingValues: PaddingValues = PaddingValues(), - player: State, + network: State, onPlayerChange: (String) -> Unit, onHostChange: (String) -> Unit, + onResetPortChange: () -> Unit, onPortChange: (String) -> Unit, + onResetHostChange: () -> Unit, onConnect: () -> Unit, onDisconnect: () -> Unit, ) { @@ -239,10 +256,10 @@ private fun NetworkContent( TextField( modifier = Modifier.fillMaxWidth(), singleLine = true, - enabled = player.value.enableFields, + enabled = network.value.enableFields, label = { Text(text = stringResource(Res.string.network__player_name__label)) }, onValueChange = { onPlayerChange(it) }, - value = player.value.player, + value = network.value.player, ) Spacer( @@ -255,24 +272,58 @@ private fun NetworkContent( TextField( modifier = Modifier.weight(1f), singleLine = true, - enabled = player.value.enableFields, + enabled = network.value.enableFields, label = { Text(text = stringResource(Res.string.network__host__label)) }, + trailingIcon = { + AnimatedVisibility( + visible = network.value.resetHost, + enter = fadeIn(), + exit = fadeOut(), + ) { + IconButton( + onClick = onResetHostChange, + ) { + Icon( + painter = painterResource(Res.drawable.ic_cancel_24dp), + tint = MaterialTheme.lwa.colorScheme.base.primary, + contentDescription = null, + ) + } + } + }, onValueChange = { onHostChange(it) }, - value = player.value.host, + value = network.value.host, ) TextField( - modifier = Modifier.width(100.dp), + modifier = Modifier.width(124.dp), singleLine = true, - enabled = player.value.enableFields, + enabled = network.value.enableFields, label = { Text(text = stringResource(Res.string.network__port__label)) }, + trailingIcon = { + AnimatedVisibility( + visible = network.value.resetPort, + enter = fadeIn(), + exit = fadeOut(), + ) { + IconButton( + onClick = onResetPortChange, + ) { + Icon( + painter = painterResource(Res.drawable.ic_cancel_24dp), + tint = MaterialTheme.lwa.colorScheme.base.primary, + contentDescription = null, + ) + } + } + }, onValueChange = { onPortChange(it) }, - value = player.value.port, + value = network.value.port, ) } TextButton( modifier = Modifier.align(alignment = Alignment.End), - enabled = player.value.enableActions, + enabled = network.value.enableActions, onClick = onConnect, ) { Text(text = stringResource(Res.string.network__socket__connect_action)) @@ -280,7 +331,7 @@ private fun NetworkContent( TextButton( modifier = Modifier.align(alignment = Alignment.End), - enabled = player.value.enableCancel, + enabled = network.value.enableCancel, onClick = onDisconnect, ) { Text(text = stringResource(Res.string.network__socket__disconnect_action)) diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/network/NetworkViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/network/NetworkViewModel.kt index dd2b7a7..1c8c2a3 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/network/NetworkViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/network/NetworkViewModel.kt @@ -8,12 +8,15 @@ import com.pixelized.desktop.lwa.repository.network.NetworkRepository import com.pixelized.desktop.lwa.repository.settings.SettingsRepository import com.pixelized.desktop.lwa.ui.composable.blur.BlurContentController import com.pixelized.desktop.lwa.ui.composable.error.ErrorSnackUio +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -23,6 +26,7 @@ class NetworkViewModel( private val factory: NetworkFactory, ) : ViewModel() { private val settings = settingsRepository.settings() + private val nameFlow = MutableStateFlow(settings.playerName) private val hostFlow = MutableStateFlow(settings.host) private val portFlow = MutableStateFlow(settings.port) @@ -40,14 +44,17 @@ class NetworkViewModel( val network: StateFlow = combine( settingsRepository.settingsFlow(), networkRepository.status, + nameFlow, hostFlow, portFlow, - ) { settings, status, host, port -> + ) { settings, status, name, host, port -> factory.convertToUio( - player = settings.playerName, + player = name, status = status, host = host, + resetHost = settings.host != host, port = port, + resetPort = settings.port != port, ) }.stateIn( scope = viewModelScope, @@ -55,29 +62,46 @@ class NetworkViewModel( initialValue = NetworkPageUio.empty() ) + init { + settingsRepository.settingsFlow().onEach { + nameFlow.value = it.playerName + hostFlow.value = it.host + portFlow.value = it.port + }.launchIn(viewModelScope) + } + fun onPlayerNameChange(player: String) { - settingsRepository.update( - settings = settingsRepository.settings().copy( - playerName = player, - ) - ) + nameFlow.value = player } fun onPortChange(port: String) { - portFlow.value = port.toIntOrNull() ?: settings.port + portFlow.value = port.toIntOrNull() ?: 0 + } + + fun onResetPortChange() { + portFlow.value = settings.port } fun onHostChange(host: String) { hostFlow.value = host } + fun onResetHostChange() { + hostFlow.value = settings.host + } + fun connect() { blurController.show() _isLoading.value = true - if (settings.host != hostFlow.value || settings.port != portFlow.value) { + if ( + settings.playerName != nameFlow.value || + settings.host != hostFlow.value || + settings.port != portFlow.value + ) { settingsRepository.update( settings = settings.copy( + playerName = nameFlow.value, host = hostFlow.value, port = portFlow.value ) @@ -94,8 +118,11 @@ class NetworkViewModel( onFailure = { _isLoading.value = false blurController.hide() - viewModelScope.launch { - _networkError.emit(ErrorSnackUio(it)) + + if (it !is CancellationException) { + viewModelScope.launch { + _networkError.emit(ErrorSnackUio.from(it)) + } } }, ) diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/roll/RollActionUio.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/roll/RollActionUio.kt deleted file mode 100644 index 0ec6dce..0000000 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/roll/RollActionUio.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.pixelized.desktop.lwa.ui.screen.roll - -import androidx.compose.runtime.Stable - -@Stable -data class RollActionUio( - val characterSheetId: String, - val label: String, - val rollAction: String, - val rollSuccessValue: Int?, -) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/roll/RollViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/roll/RollViewModel.kt deleted file mode 100644 index 7c6fdfa..0000000 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/roll/RollViewModel.kt +++ /dev/null @@ -1,253 +0,0 @@ -package com.pixelized.desktop.lwa.ui.screen.roll - -import androidx.compose.animation.core.Animatable -import androidx.compose.animation.core.Spring -import androidx.compose.animation.core.spring -import androidx.compose.runtime.State -import androidx.compose.runtime.mutableStateOf -import androidx.lifecycle.ViewModel -import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository -import com.pixelized.desktop.lwa.repository.network.NetworkRepository -import com.pixelized.desktop.lwa.repository.settings.SettingsRepository -import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.CharacterSheetPageUio -import com.pixelized.desktop.lwa.ui.screen.roll.DifficultyUio.Difficulty -import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet -import com.pixelized.shared.lwa.protocol.websocket.payload.RollMessage -import com.pixelized.shared.lwa.usecase.ExpressionUseCase -import com.pixelized.shared.lwa.usecase.SkillStepUseCase -import kotlinx.coroutines.Job -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import lwacharactersheet.composeapp.generated.resources.Res -import lwacharactersheet.composeapp.generated.resources.roll_page__critical_failure -import lwacharactersheet.composeapp.generated.resources.roll_page__critical_success -import lwacharactersheet.composeapp.generated.resources.roll_page__dc_easy__label -import lwacharactersheet.composeapp.generated.resources.roll_page__dc_hard__label -import lwacharactersheet.composeapp.generated.resources.roll_page__dc_impossible__label -import lwacharactersheet.composeapp.generated.resources.roll_page__dc_normal__label -import lwacharactersheet.composeapp.generated.resources.roll_page__failure -import lwacharactersheet.composeapp.generated.resources.roll_page__special_success -import lwacharactersheet.composeapp.generated.resources.roll_page__success -import org.jetbrains.compose.resources.getString - -class RollViewModel( - private val characterSheetRepository: CharacterSheetRepository, - private val settingsRepository: SettingsRepository, - private val skillComputation: ExpressionUseCase, - private val skillStepUseCase: SkillStepUseCase, - private val networkRepository: NetworkRepository, -) : ViewModel() { - - private lateinit var sheet: CharacterSheet - private lateinit var rollAction: String - private var rollSuccessValue: Int? = null - - private var rollJob: Job? = null - - private val _rollTitle = mutableStateOf(RollTitleUio(label = "", value = 0)) - val rollTitle: State get() = _rollTitle - - private val _rollResult = mutableStateOf(null) - val rollResult: State get() = _rollResult - - val rollRotation = Animatable(0f) - val rollScale = Animatable(1f) - - private val _rollDifficulty = mutableStateOf(null) - val rollDifficulty: State get() = _rollDifficulty - - private val _displayOverlay = mutableStateOf(false) - val displayOverlay: State get() = _displayOverlay - - @Deprecated(message = "@See prepareRoll(RollActionUio)") - fun prepareRoll( - sheet: CharacterSheetPageUio, - roll: CharacterSheetPageUio.Roll, - ) { - prepareRoll( - characterSheetId = sheet.id, - label = roll.label, - rollAction = roll.value, - rollSuccessValue = null, - ) - } - - fun prepareRoll( - roll: RollActionUio, - ) = prepareRoll( - characterSheetId = roll.characterSheetId, - label = roll.label, - rollAction = roll.rollAction, - rollSuccessValue = roll.rollSuccessValue, - ) - - private fun prepareRoll( - characterSheetId: String, - label: String, - rollAction: String, - rollSuccessValue: Int?, - ) { - this.sheet = runBlocking { - rollRotation.snapTo(0f) - rollScale.snapTo(1f) - characterSheetRepository.characterDetail(characterSheetId = characterSheetId) - } ?: return - - this.rollAction = rollAction - this.rollSuccessValue = rollSuccessValue - - val rollStep = rollSuccessValue?.let { - skillStepUseCase.computeSkillStep(skill = it) - } - - _rollResult.value = null - _rollTitle.value = RollTitleUio( - label = label, - value = rollStep?.success?.last - ) - _rollDifficulty.value = rollSuccessValue?.let { - DifficultyUio( - open = false, - Difficulty.NORMAL, - ) - } - } - - suspend fun roll() { - coroutineScope { - _rollResult.value = null - - rollJob?.cancel() - rollJob = launch { - launch { - rollScale.animateTo( - targetValue = 1.20f, - animationSpec = spring( - dampingRatio = Spring.DampingRatioNoBouncy, - stiffness = 800f, - ) - ) - rollScale.animateTo( - targetValue = 1f, - animationSpec = spring( - dampingRatio = 0.28f, - stiffness = 800f, - ) - ) - } - launch { - rollRotation.animateTo( - targetValue = rollRotation.value.let { it - it % 360 } + 360f * 3, - animationSpec = spring( - dampingRatio = Spring.DampingRatioNoBouncy, - stiffness = Spring.StiffnessLow, - ) - ) - } - launch { - delay(500) - - val rollStep = rollSuccessValue?.let { - skillStepUseCase.computeSkillStep( - skill = when (_rollDifficulty.value?.difficulty) { - Difficulty.EASY -> it * 2 - Difficulty.NORMAL -> it - Difficulty.HARD -> it / 2 - Difficulty.IMPOSSIBLE -> it / 4 - else -> it - } - ) - } - - val roll = skillComputation.computeRoll( - sheet = sheet, - alterations = emptyMap(), // TODO ? - expression = rollAction, - ) - - val success = rollStep?.let { - when (roll) { - in it.criticalSuccess -> getString(resource = Res.string.roll_page__critical_success) - in it.specialSuccess -> getString(resource = Res.string.roll_page__special_success) - in it.success -> getString(resource = Res.string.roll_page__success) - in it.failure -> getString(resource = Res.string.roll_page__failure) - in it.criticalFailure -> getString(resource = Res.string.roll_page__critical_failure) - else -> "" - } - } - - _rollResult.value = RollResultUio( - label = success ?: "", - value = roll, - ) - launch { - val payload = RollMessage( - characterId = sheet.id, - skillLabel = _rollTitle.value.label, - rollDifficulty = when (_rollDifficulty.value?.difficulty) { - Difficulty.EASY -> getString(Res.string.roll_page__dc_easy__label) - Difficulty.NORMAL -> getString(Res.string.roll_page__dc_normal__label) - Difficulty.HARD -> getString(Res.string.roll_page__dc_hard__label) - Difficulty.IMPOSSIBLE -> getString(Res.string.roll_page__dc_impossible__label) - else -> null - }, - rollValue = roll, - rollSuccessLimit = rollStep?.success?.last, - resultLabel = success, - critical = rollStep?.let { - when (roll) { - in it.criticalSuccess -> RollMessage.Critical.CRITICAL_SUCCESS - in it.specialSuccess -> RollMessage.Critical.SPECIAL_SUCCESS - in it.success -> RollMessage.Critical.SUCCESS - in it.failure -> RollMessage.Critical.FAILURE - in it.criticalFailure -> RollMessage.Critical.CRITICAL_FAILURE - else -> null - } - } - ) - networkRepository.share( - payload = payload, - ) - } - } - } - } - } - - fun toggleDifficulty() { - _rollDifficulty.value = _rollDifficulty.value?.copy( - open = _rollDifficulty.value?.open?.not() ?: false - ) - } - - fun onDifficulty(difficulty: Difficulty) { - _rollDifficulty.value = DifficultyUio( - open = false, - difficulty = difficulty, - ) - val rollStep = rollSuccessValue?.let { - skillStepUseCase.computeSkillStep( - skill = when (_rollDifficulty.value?.difficulty) { - Difficulty.EASY -> it * 2 - Difficulty.NORMAL -> it - Difficulty.HARD -> it / 2 - Difficulty.IMPOSSIBLE -> it / 4 - else -> it - } - ) - } - _rollTitle.value = _rollTitle.value.copy( - value = rollStep?.success?.last - ) - } - - fun showOverlay() { - _displayOverlay.value = true - } - - fun hideOverlay() { - _displayOverlay.value = false - } -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/rollhistory/RollHistoryViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/rollhistory/RollHistoryViewModel.kt index 265ef37..2547c1c 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/rollhistory/RollHistoryViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/rollhistory/RollHistoryViewModel.kt @@ -18,7 +18,7 @@ class RollHistoryViewModel( characterRepository.characterSheetPreviewFlow, rollRepository.rolls, ) { sheets, message -> - val name = sheets.firstOrNull { it.id == message.characterId }?.name ?: "" + val name = sheets.firstOrNull { it.id == message.characterSheetId }?.name ?: "" val roll = RollHistoryItemUio( character = name, skillLabel = message.skillLabel, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/settings/SettingsScreen.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/settings/SettingsScreen.kt index a41d28a..44a9cad 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/settings/SettingsScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/settings/SettingsScreen.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.type import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.navigation.NavHostController import com.pixelized.desktop.lwa.ui.composable.key.KeyHandler import com.pixelized.desktop.lwa.ui.navigation.screen.LocalScreenController import com.pixelized.desktop.lwa.ui.navigation.screen.destination.SettingsDestination @@ -55,7 +56,7 @@ fun SettingsScreen( KeyHandler { when { it.type == KeyEventType.KeyUp && it.key == Key.Escape -> { - screen.popBackStack(route = SettingsDestination.baseRoute(), inclusive = true) + screen.navigateBack() true } @@ -68,7 +69,7 @@ fun SettingsScreen( modifier = Modifier.fillMaxSize(), items = viewModel.items, onBack = { - screen.popBackStack(route = SettingsDestination.baseRoute(), inclusive = true) + screen.navigateBack() }, onReset = viewModel::onReset ) @@ -127,4 +128,9 @@ private fun SettingsContent( } } ) -} \ No newline at end of file +} + +private fun NavHostController.navigateBack() = popBackStack( + route = SettingsDestination.baseRoute(), + inclusive = true, +) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/settings/SettingsViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/settings/SettingsViewModel.kt index 56b3926..d3f374f 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/settings/SettingsViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/settings/SettingsViewModel.kt @@ -107,8 +107,16 @@ class SettingsViewModel( } } + /** + * Reset the current settings except the player name. + */ fun onReset() { - settingsRepository.update(settings = settingsUseCase.defaultSettings()) + val current = settingsRepository.settings() + settingsRepository.update( + settings = settingsUseCase.defaultSettings().copy( + playerName = current.playerName, + ) + ) } private val HashMap>.dynamicDice diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/LwaTheme.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/LwaTheme.kt index 97ef035..9438901 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/LwaTheme.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/LwaTheme.kt @@ -10,6 +10,8 @@ import com.pixelized.desktop.lwa.ui.theme.color.LwaColors import com.pixelized.desktop.lwa.ui.theme.color.darkLwaColorTheme import com.pixelized.desktop.lwa.ui.theme.shapes.LwaShapes import com.pixelized.desktop.lwa.ui.theme.shapes.lwaShapes +import com.pixelized.desktop.lwa.ui.theme.size.LwaSize +import com.pixelized.desktop.lwa.ui.theme.size.lwaSize import com.pixelized.desktop.lwa.ui.theme.typography.LwaTypography import com.pixelized.desktop.lwa.ui.theme.typography.lwaTypography @@ -27,6 +29,7 @@ data class LwaTheme( val colorScheme: LwaColors, val typography: LwaTypography, val shapes: LwaShapes, + val size: LwaSize, ) @Composable @@ -36,12 +39,14 @@ fun LwaTheme( val lwaColors = darkLwaColorTheme() val lwaTypography = lwaTypography(colors = lwaColors) val lwaShapes = lwaShapes() + val lwaSize = lwaSize() val theme = remember { LwaTheme( colorScheme = lwaColors, typography = lwaTypography, shapes = lwaShapes, + size = lwaSize, ) } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/color/LwaColors.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/color/LwaColors.kt index 73f4fd0..e2c5242 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/color/LwaColors.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/color/LwaColors.kt @@ -16,6 +16,7 @@ import kotlin.math.ln data class LwaColors( val base: Colors, val elevated: Elevated, + val portrait: Portrait, val portraitBackgroundBrush: Brush, val chat: Chat, ) { @@ -27,6 +28,11 @@ data class LwaColors( val base4dp: Color, ) + @Stable + data class Portrait( + val levelUp: Color, + ) + @Stable data class Chat( val timestamp: Color, @@ -74,6 +80,9 @@ fun darkLwaColorTheme( elevated.base1dp.copy(alpha = 0.8f), ) ), + portrait: LwaColors.Portrait = LwaColors.Portrait( + levelUp = Color(0xFFffe900), + ), chat: LwaColors.Chat = LwaColors.Chat( timestamp = base.secondary, text = base.onSurface.copy(alpha = 0.7f), @@ -86,6 +95,7 @@ fun darkLwaColorTheme( ): LwaColors = LwaColors( base = base, elevated = elevated, + portrait = portrait, portraitBackgroundBrush = portraitBackgroundBrush, chat = chat, ) diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/shapes/ArrowShape.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/shapes/ArrowShape.kt new file mode 100644 index 0000000..789bf01 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/shapes/ArrowShape.kt @@ -0,0 +1,83 @@ +package com.pixelized.desktop.lwa.ui.theme.shapes + +import androidx.compose.runtime.Stable +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.LayoutDirection + +@Stable +class ArrowShape( + private val core: Dp, + private val head: Dp, +) : Shape { + + override fun createOutline( + size: Size, + layoutDirection: LayoutDirection, + density: Density, + ): Outline { + return Outline.Generic( + path = drawArrowPath( + size = size, + core = with(density) { core.toPx() }, + head = with(density) { head.toPx() }, + ) + ) + } + + private fun drawArrowPath( + size: Size, + core: Float, + head: Float, + ): Path { + return Path().apply { + reset() + + moveTo( + x = size.width / 2f, + y = 0f, + ) + + lineTo( + x = size.width / 2f, + y = 0f, + ) + + lineTo( + x = size.width / 2f + core + head, + y = core + head, + ) + + lineTo( + x = size.width / 2f + core, + y = core + head, + ) + + lineTo( + x = size.width / 2f + core, + y = size.height, + ) + + lineTo( + x = size.width / 2f - core, + y = size.height, + ) + + lineTo( + x = size.width / 2f - core, + y = core + head, + ) + + lineTo( + x = size.width / 2f - core - head, + y = core + head, + ) + + close() + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/shapes/LwaShapes.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/shapes/LwaShapes.kt index a139ba1..c53ed56 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/shapes/LwaShapes.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/shapes/LwaShapes.kt @@ -9,15 +9,18 @@ import androidx.compose.ui.unit.dp @Stable data class LwaShapes( + val panel: Shape, val settings: Shape, ) @Stable @Composable fun lwaShapes( + panel: Shape = RoundedCornerShape(16.dp), settings: Shape = RoundedCornerShape(8.dp), ): LwaShapes = remember { LwaShapes( + panel = panel, settings = settings, ) } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/size/LwaSize.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/size/LwaSize.kt new file mode 100644 index 0000000..9149318 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/size/LwaSize.kt @@ -0,0 +1,22 @@ +package com.pixelized.desktop.lwa.ui.theme.size + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp + +@Stable +data class LwaSize( + val characteristic: DpSize, +) + +@Composable +@Stable +fun lwaSize( + characteristic: DpSize = DpSize(width = 76.dp, height = 110.dp), +) = remember { + LwaSize( + characteristic = characteristic, + ) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/utils/extention/JsonExt.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/utils/extention/JsonExt.kt deleted file mode 100644 index d746fc2..0000000 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/utils/extention/JsonExt.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.pixelized.desktop.lwa.utils.extention - -import com.pixelized.shared.lwa.protocol.websocket.Message -import io.ktor.websocket.Frame -import io.ktor.websocket.readText -import kotlinx.serialization.json.Json - -fun Json.decodeFromFrame(frame: Frame.Text): Message { - val json = frame.readText() - return decodeFromString(json) -} - -fun Json.encodeToFrame(message: Message): Frame { - val json = encodeToString(message) - return Frame.Text(text = json) -} \ No newline at end of file diff --git a/composeApp/src/commonTest/kotlin/com/pixelized/desktop/lwa/business/DamageBonusUseCaseTest.kt b/composeApp/src/commonTest/kotlin/com/pixelized/desktop/lwa/business/DamageBonusUseCaseTest.kt index f2f8fb9..bcb361c 100644 --- a/composeApp/src/commonTest/kotlin/com/pixelized/desktop/lwa/business/DamageBonusUseCaseTest.kt +++ b/composeApp/src/commonTest/kotlin/com/pixelized/desktop/lwa/business/DamageBonusUseCaseTest.kt @@ -10,42 +10,42 @@ class DamageBonusUseCaseTest { val userCase = CharacterSheetUseCase() (0 until 12).forEach { - val result = userCase.damageBonus(sum = it) + val result = userCase.meleeBonusDamage(sum = it) val expected = "-1d6" assert(result == expected) { "Expected:'$expected' bonus damage for stat:'$it' but was:'$result'" } } (12 until 18).forEach { - val result = userCase.damageBonus(sum = it) + val result = userCase.meleeBonusDamage(sum = it) val expected = "-1d4" assert(result == expected) { "Expected:'$expected' bonus damage for stat:'$it' but was:'$result'" } } (18 until 23).forEach { - val result = userCase.damageBonus(sum = it) + val result = userCase.meleeBonusDamage(sum = it) val expected = "+0" assert(result == expected) { "Expected:'$expected' bonus damage for stat:'$it' but was:'$result'" } } (23 until 30).forEach { - val result = userCase.damageBonus(sum = it) + val result = userCase.meleeBonusDamage(sum = it) val expected = "+1d4" assert(result == expected) { "Expected:'$expected' bonus damage for stat:'$it' but was:'$result'" } } (30 until 40).forEach { - val result = userCase.damageBonus(sum = it) + val result = userCase.meleeBonusDamage(sum = it) val expected = "+1d6" assert(result == expected) { "Expected:'$expected' bonus damage for stat:'$it' but was:'$result'" } } (40 until 100).forEach { - val result = userCase.damageBonus(sum = it) + val result = userCase.meleeBonusDamage(sum = it) val expected = "+2d6" assert(result == expected) { "Expected:'$expected' bonus damage for stat:'$it' but was:'$result'" diff --git a/composeApp/src/commonTest/kotlin/com/pixelized/desktop/lwa/parser/expression/ExpressionParserTest.kt b/composeApp/src/commonTest/kotlin/com/pixelized/desktop/lwa/parser/expression/ExpressionParserTest.kt index ca059e3..d33d499 100644 --- a/composeApp/src/commonTest/kotlin/com/pixelized/desktop/lwa/parser/expression/ExpressionParserTest.kt +++ b/composeApp/src/commonTest/kotlin/com/pixelized/desktop/lwa/parser/expression/ExpressionParserTest.kt @@ -4,6 +4,7 @@ import com.pixelized.shared.lwa.parser.dice.DiceParser import com.pixelized.shared.lwa.parser.expression.Expression import com.pixelized.shared.lwa.parser.expression.ExpressionParser import com.pixelized.shared.lwa.parser.expression.ExpressionParser.Error +import com.pixelized.shared.lwa.parser.word.Word import com.pixelized.shared.lwa.parser.word.WordParser import org.junit.Test import kotlin.test.assertFailsWith @@ -18,9 +19,6 @@ class ExpressionParserTest { ) parser.test("", null) parser.test(" ", null) - assertFailsWith(Error.UnRecognizedToken::class) { - parser.test("pouet", null) - } assertFailsWith(Error.ExpectedTokenCharacter::class) { parser.test("1+", null) } @@ -98,6 +96,10 @@ class ExpressionParserTest { expression = "max(1,2)", expected = Expression.Maximum(Expression.Flat(1), Expression.Flat(2)) ) + parser.test( + expression = "floor5(13)", + expected = Expression.Floor5(Expression.Flat(13)) + ) } @Test @@ -122,6 +124,50 @@ class ExpressionParserTest { ) } + @Test + fun testWordExpression() { + val parser = ExpressionParser( + diceParser = DiceParser(), + wordParser = WordParser(), + ) + parser.test( + expression = "BDC", + expected = Expression.WordExpression(Word(Word.Type.BDC)), + ) + parser.test( + expression = "BDD", + expected = Expression.WordExpression(Word(Word.Type.BDD)), + ) + parser.test( + expression = "STR", + expected = Expression.WordExpression(Word(Word.Type.STR)), + ) + parser.test( + expression = "DEX", + expected = Expression.WordExpression(Word(Word.Type.DEX)), + ) + parser.test( + expression = "CON", + expected = Expression.WordExpression(Word(Word.Type.CON)), + ) + parser.test( + expression = "HEI", + expected = Expression.WordExpression(Word(Word.Type.HEI)), + ) + parser.test( + expression = "INT", + expected = Expression.WordExpression(Word(Word.Type.INT)), + ) + parser.test( + expression = "POW", + expected = Expression.WordExpression(Word(Word.Type.POW)), + ) + parser.test( + expression = "CHA", + expected = Expression.WordExpression(Word(Word.Type.CHA)), + ) + } + @Test fun testReadWrite() { val parser = ExpressionParser( @@ -142,7 +188,7 @@ class ExpressionParserTest { ) { val result = parse(input = expression) assert(result == expected) { - "ExpressionParser.parse(input=$expression) is expected to return:$expected, but was:$result" + "ExpressionParser.parse(input=$expression) is expected to return:$expected, but was:$result type:${result?.let { it::class.java.simpleName }}" } } diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/extention/JsonExt.kt b/server/src/main/kotlin/com/pixelized/server/lwa/extention/JsonExt.kt deleted file mode 100644 index 35f8daa..0000000 --- a/server/src/main/kotlin/com/pixelized/server/lwa/extention/JsonExt.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.pixelized.server.lwa.extention - -import com.pixelized.shared.lwa.protocol.websocket.Message -import io.ktor.websocket.Frame -import io.ktor.websocket.readText -import kotlinx.serialization.json.Json - - -fun Json.decodeFromFrame(frame: Frame.Text): Message { - val json = frame.readText() - return decodeFromString(json) -} - -fun Json.encodeToFrame(message: Message): Frame { - val json = encodeToString(message) - return Frame.Text(text = json) -} \ No newline at end of file diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/campaign/CampaignService.kt b/server/src/main/kotlin/com/pixelized/server/lwa/model/campaign/CampaignService.kt index f0cdf5a..2661132 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/campaign/CampaignService.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/model/campaign/CampaignService.kt @@ -204,6 +204,5 @@ class CampaignService( ) } } - } } \ No newline at end of file diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/Server.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/Server.kt index c7f284e..149dff5 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/server/Server.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/Server.kt @@ -1,8 +1,6 @@ package com.pixelized.server.lwa.server -import com.pixelized.server.lwa.extention.decodeFromFrame -import com.pixelized.server.lwa.extention.encodeToFrame import com.pixelized.server.lwa.server.rest.alteration.getActiveAlteration import com.pixelized.server.lwa.server.rest.alteration.getAlteration import com.pixelized.server.lwa.server.rest.alteration.putActiveAlteration @@ -17,6 +15,7 @@ import com.pixelized.server.lwa.server.rest.character.getCharacter import com.pixelized.server.lwa.server.rest.character.getCharacters import com.pixelized.server.lwa.server.rest.character.putCharacter import com.pixelized.shared.lwa.SERVER_PORT +import com.pixelized.shared.lwa.protocol.websocket.Message import com.pixelized.shared.lwa.sharedModuleDependencies import io.ktor.serialization.kotlinx.json.json import io.ktor.server.application.install @@ -35,6 +34,7 @@ import io.ktor.server.websocket.pingPeriod import io.ktor.server.websocket.timeout import io.ktor.server.websocket.webSocket import io.ktor.websocket.Frame +import io.ktor.websocket.readText import kotlinx.coroutines.channels.consumeEach import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -82,15 +82,22 @@ class LocalServer { val job = launch { // send local message to the clients engine.webSocket.collect { message -> - val frame = json.encodeToFrame(message) - send(frame) + val data = json.encodeToString(message) + val frame = Frame.Text(text = data) + try { + send(frame) + } catch (exception : Exception) { + // TODO + println("WebSocket exception: ${exception.localizedMessage}") + } } } runCatching { // watching for clients incoming message incoming.consumeEach { frame -> if (frame is Frame.Text) { - val message = Json.decodeFromFrame(frame = frame) + val data = frame.readText() + val message = json.decodeFromString(data) // log the message engine.handle(message) // broadcast to clients the message @@ -98,6 +105,7 @@ class LocalServer { } } }.onFailure { exception -> + // TODO println("WebSocket exception: ${exception.localizedMessage}") }.also { job.cancel() diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/AlteredCharacterSheet.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/AlteredCharacterSheet.kt index b3d20a8..9d95aee 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/AlteredCharacterSheet.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/AlteredCharacterSheet.kt @@ -50,6 +50,8 @@ class AlteredCharacterSheet( val name: String = sheet.name + val shouldLevelUp: Boolean = sheet.shouldLevelUp + val portrait: String? get() = alterations[PORTRAIT] ?.firstNotNullOfOrNull { (it.expression as? Expression.UrlExpression)?.url } @@ -104,7 +106,7 @@ class AlteredCharacterSheet( val damageBonus: String get() { - val initial = sheetUseCase.damageBonus( + val initial = sheetUseCase.meleeBonusDamage( strength = strength, height = height, ) @@ -128,8 +130,7 @@ class AlteredCharacterSheet( private fun List?.sum() = this?.sumOf { expressionUseCase.computeExpression( - sheet = sheet, - alterations = alterations, + sheet = this@AlteredCharacterSheet, expression = it.expression ) } ?: 0 diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/characterSheet/CharacterSheet.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/characterSheet/CharacterSheet.kt index 95a9284..97652c9 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/characterSheet/CharacterSheet.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/characterSheet/CharacterSheet.kt @@ -6,6 +6,7 @@ data class CharacterSheet( val portrait: String?, val thumbnail: String?, val level: Int, + val shouldLevelUp: Boolean, // characteristics val strength: Int, val dexterity: Int, diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/characterSheet/CharacterSheetJsonFactory.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/characterSheet/CharacterSheetJsonFactory.kt index c5c75da..669fba3 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/characterSheet/CharacterSheetJsonFactory.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/characterSheet/CharacterSheetJsonFactory.kt @@ -24,6 +24,7 @@ class CharacterSheetJsonFactory( portrait = json.portrait, thumbnail = json.thumbnail, level = json.level, + shouldLevelUp = json.shouldLevelUp ?: false, strength = json.strength, dexterity = json.dexterity, constitution = json.constitution, @@ -100,6 +101,7 @@ class CharacterSheetJsonFactory( thumbnail = sheet.thumbnail, portrait = sheet.portrait, level = sheet.level, + shouldLevelUp = sheet.shouldLevelUp, strength = sheet.strength, dexterity = sheet.dexterity, constitution = sheet.constitution, diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/characterSheet/CharacterSheetJsonV1.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/characterSheet/CharacterSheetJsonV1.kt index 11859f4..e4eb980 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/characterSheet/CharacterSheetJsonV1.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/characterSheet/CharacterSheetJsonV1.kt @@ -9,6 +9,7 @@ data class CharacterSheetJsonV1( val portrait: String?, val thumbnail: String?, val level: Int, + val shouldLevelUp: Boolean?, // characteristics val strength: Int, val dexterity: Int, diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/parser/expression/Expression.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/parser/expression/Expression.kt index 5281752..27706b9 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/parser/expression/Expression.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/parser/expression/Expression.kt @@ -59,6 +59,14 @@ sealed interface Expression { } } + data class Floor5( + val expression: Expression?, + ) : Expression { + override fun toString(): String { + return "floor5($expression)" + } + } + data class Inversion( val expression: Expression, ) : Expression { diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/parser/expression/ExpressionParser.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/parser/expression/ExpressionParser.kt index 107c671..3a99693 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/parser/expression/ExpressionParser.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/parser/expression/ExpressionParser.kt @@ -180,6 +180,25 @@ class ExpressionParser( return Expression.Maximum(first, second) } + "floor5" -> { + // consume the '(' character + stack.moveCursor() + // evaluate the content of the first parameter. + val expression = evaluate() + // check that the expression is well formed, need a ). + guard(stack.peek() == ')') { + Error.ExpectedOperator( + expected = ')', + actual = stack.peek(), + expression = stack.input + ) + } + // consume the ')' character + stack.moveCursor() + // build the final function expression + return Expression.Floor5(expression) + } + else -> { val value = token.toIntOrNull() if (value != null) { diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/payload/RollMessage.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/payload/RollMessage.kt index 34a44d8..2b3716d 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/payload/RollMessage.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/payload/RollMessage.kt @@ -1,17 +1,36 @@ package com.pixelized.shared.lwa.protocol.websocket.payload import kotlinx.serialization.Serializable +import java.util.UUID @Serializable data class RollMessage( - val characterId: String, + val id: RollId, + val characterSheetId: String, val skillLabel: String, + val rollValue: Int, val resultLabel: String? = null, val rollDifficulty: String? = null, - val rollValue: Int, val rollSuccessLimit: Int? = null, - val critical: Critical?, + val critical: Critical? = null, ) : MessagePayload { + + @Serializable + data class RollId( + val rollId: String, + val timestamp: Long, + ) { + companion object { + fun create( + rollId: String = UUID.randomUUID().toString(), + timestamp: Long = System.currentTimeMillis(), + ) = RollId( + rollId = rollId, + timestamp = timestamp, + ) + } + } + enum class Critical { CRITICAL_SUCCESS, SPECIAL_SUCCESS, diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/usecase/CharacterSheetUseCase.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/usecase/CharacterSheetUseCase.kt index 567384b..1ac6f92 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/usecase/CharacterSheetUseCase.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/usecase/CharacterSheetUseCase.kt @@ -27,14 +27,14 @@ class CharacterSheetUseCase { return power } - fun damageBonus( + fun meleeBonusDamage( strength: Int, height: Int, ): String { - return damageBonus(sum = strength + height) + return meleeBonusDamage(sum = strength + height) } - fun damageBonus( + fun meleeBonusDamage( sum: Int, ): String { return when { @@ -47,6 +47,26 @@ class CharacterSheetUseCase { } } + fun distanceBonusDamage( + strength: Int, + height: Int, + ): String { + return distanceBonusDamage(sum = strength + height) + } + + fun distanceBonusDamage( + sum: Int, + ): String { + return when { + sum < 12 -> "-1d3" + sum in 12..17 -> "-1d2" + sum in 18..22 -> "+0" + sum in 23..29 -> "+1d2" + sum in 30..39 -> "+1d3" + else -> "+2d3" + } + } + fun armor(): Int = 0 fun learning(intelligence: Int): Int { diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/usecase/ExpressionUseCase.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/usecase/ExpressionUseCase.kt index 54b143a..7baf5f4 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/usecase/ExpressionUseCase.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/usecase/ExpressionUseCase.kt @@ -1,15 +1,9 @@ package com.pixelized.shared.lwa.usecase +import com.pixelized.shared.lwa.model.AlteredCharacterSheet import com.pixelized.shared.lwa.model.alteration.FieldAlteration import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet -import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.CHA -import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.CON -import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.DEX -import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.HEI -import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.INT -import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.POW -import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.STR import com.pixelized.shared.lwa.parser.expression.Expression import com.pixelized.shared.lwa.parser.expression.ExpressionParser import com.pixelized.shared.lwa.parser.word.Word @@ -22,15 +16,13 @@ class ExpressionUseCase( private val rollUseCase: RollUseCase, ) { fun computeSkillValue( - sheet: CharacterSheet, + sheet: AlteredCharacterSheet, + skill: CharacterSheet.Skill, alterations: Map>, diminished: Int, - skill: CharacterSheet.Skill, ): Int { val context = Context( sheet = sheet, - skill = skill, - alterations = alterations, ) val base: Int = context.evaluate( @@ -56,15 +48,13 @@ class ExpressionUseCase( } fun computeRoll( - sheet: CharacterSheet, - alterations: Map>, + sheet: AlteredCharacterSheet, expression: String, ): Int { print("Roll::$expression::") val roll = expressionParser.parse(input = expression)?.let { computeExpression( sheet = sheet, - alterations = alterations, expression = it, ) } ?: 0 @@ -73,14 +63,11 @@ class ExpressionUseCase( } fun computeExpression( - sheet: CharacterSheet, - alterations: Map>, + sheet: AlteredCharacterSheet, expression: Expression, ): Int { val context = Context( sheet = sheet, - skill = null, - alterations = alterations, ) return context.evaluate( expression = expression, @@ -113,11 +100,15 @@ class ExpressionUseCase( } is Expression.Maximum -> { - min(evaluate(expression.first), evaluate(expression.second)) + max(evaluate(expression.first), evaluate(expression.second)) } is Expression.Minimum -> { - max(evaluate(expression.first), evaluate(expression.second)) + min(evaluate(expression.first), evaluate(expression.second)) + } + + is Expression.Floor5 -> { + evaluate(expression.expression).let { it - it % 5 } } is Expression.Flat -> { @@ -135,29 +126,29 @@ class ExpressionUseCase( is Expression.WordExpression -> when (expression.word.type) { Word.Type.BDC -> evaluate( expression = expressionParser.parse( - characterSheetUseCase.damageBonus( - strength = sheet.strength + alterations[STR].sum(), - height = sheet.height + alterations[HEI].sum(), + characterSheetUseCase.meleeBonusDamage( + strength = sheet.strength, + height = sheet.height, ) ) ) Word.Type.BDD -> evaluate( expression = expressionParser.parse( - characterSheetUseCase.damageBonus( - strength = sheet.strength + alterations[STR].sum(), - height = sheet.height + alterations[HEI].sum(), + characterSheetUseCase.distanceBonusDamage( + strength = sheet.strength, + height = sheet.height, ) ) ) - Word.Type.STR -> sheet.strength + alterations[STR].sum() - Word.Type.DEX -> sheet.dexterity + alterations[DEX].sum() - Word.Type.CON -> sheet.constitution + alterations[CON].sum() - Word.Type.HEI -> sheet.height + alterations[HEI].sum() - Word.Type.INT -> sheet.intelligence + alterations[INT].sum() - Word.Type.POW -> sheet.power + alterations[POW].sum() - Word.Type.CHA -> sheet.charisma + alterations[CHA].sum() + Word.Type.STR -> sheet.strength + Word.Type.DEX -> sheet.dexterity + Word.Type.CON -> sheet.constitution + Word.Type.HEI -> sheet.height + Word.Type.INT -> sheet.intelligence + Word.Type.POW -> sheet.power + Word.Type.CHA -> sheet.charisma } null -> 0 @@ -165,9 +156,7 @@ class ExpressionUseCase( } data class Context( - val sheet: CharacterSheet, - val skill: CharacterSheet.Skill?, - val alterations: Map>, + val sheet: AlteredCharacterSheet, ) companion object {