From 29747dcb5cf1fe46238a995b96d53967caace954 Mon Sep 17 00:00:00 2001 From: Thomas Andres Gomez Date: Wed, 26 Feb 2025 14:43:42 +0100 Subject: [PATCH] Add the alteration system to the server & link the app on it. --- .../com/pixelized/desktop/lwa/Module.kt | 25 +-- .../desktop/lwa/business/ExpressionUseCase.kt | 130 ------------- .../desktop/lwa/network/LwaClient.kt | 19 +- .../desktop/lwa/network/LwaClientImpl.kt | 53 +++++- .../alteration/AlterationRepository.kt | 110 ++++++----- .../repository/alteration/AlterationStore.kt | 169 ++++++++++------- .../repository/alteration/model/Alteration.kt | 14 -- .../alteration/model/AlterationMetadata.kt | 6 - .../alteration/model/FieldAlteration.kt | 9 - .../repository/campaign/CampaignRepository.kt | 10 +- .../lwa/repository/campaign/CampaignStore.kt | 8 +- .../CharacterSheetRepository.kt | 4 +- .../repository/settings/SettingsFactory.kt | 2 +- .../repository/settings/SettingsRepository.kt | 2 +- .../lwa/repository/settings/SettingsStore.kt | 3 +- ...cterDetailCharacteristicDialogViewModel.kt | 100 ++++++++++ .../CharacterSheetCharacteristicDialog.kt} | 17 +- ...aracterSheetCharacteristicDialogFactory.kt | 70 +++++++ .../destination/CharacterSheetDestination.kt | 31 +++- .../destination/CharacterSheetWindow.kt | 5 +- .../lwa/ui/screen/campaign/CampaignScreen.kt | 2 +- .../campaign/player/detail/CharacterDetail.kt | 104 +++++++---- .../player/detail/CharacterDetailFactory.kt | 48 ++--- .../player/detail/CharacterDetailViewModel.kt | 85 +++------ .../detail/CharacterDiminishedViewModel.kt | 9 +- .../campaign/player/ribbon/PlayerPortrait.kt | 6 +- .../campaign/player/ribbon/PlayerRibbon.kt | 6 +- .../player/ribbon/PlayerRibbonFactory.kt | 42 ++--- .../player/ribbon/PlayerRibbonViewModel.kt | 20 +- .../detail/CharacterSheetFactory.kt | 77 ++++---- .../detail/CharacterSheetPage.kt | 34 +--- .../detail/CharacterSheetViewModel.kt | 98 +--------- .../detail/dialog/DiminishedStatDialog.kt | 3 +- .../edit/CharacterSheetEditFactory.kt | 16 +- .../edit/CharacterSheetEditViewModel.kt | 4 +- .../desktop/lwa/ui/screen/main/MainPage.kt | 9 +- .../lwa/ui/screen/main/MainPageViewModel.kt | 44 +++-- .../lwa/ui/screen/roll/RollViewModel.kt | 7 +- .../{business => usecase}/SettingsUseCase.kt | 2 +- .../lwa/business/DamageBonusUseCaseTest.kt | 12 +- .../desktop/lwa/business/RollUseCaseTest.kt | 1 + .../lwa/business/SkillStepUseCaseTest.kt | 3 +- .../desktop/lwa/parser/dice/DiceParserTest.kt | 2 + .../parser/expression/ExpressionParserTest.kt | 31 +++- .../desktop/lwa/parser/word/WordParserTest.kt | 2 + server/src/main/kotlin/Module.kt | 4 + .../lwa/model/alteration/AlterationService.kt | 56 ++++++ .../lwa/model/alteration/AlterationStore.kt | 144 +++++++++++++++ .../lwa/model/campaign/CampaignService.kt | 73 +++++--- .../lwa/model/campaign/CampaignStore.kt | 14 +- .../com/pixelized/server/lwa/server/Engine.kt | 18 +- .../com/pixelized/server/lwa/server/Server.kt | 17 ++ .../rest/alteration/GET_Active_Alteration.kt | 26 +++ .../server/rest/alteration/GET_Alteration.kt | 10 + .../rest/alteration/PUT_ActiveAlteration.kt | 63 +++++++ .../campaign/DELETE_Campaign_character.kt | 12 +- .../rest/campaign/DELETE_Campaign_npc.kt | 12 +- .../rest/campaign/PUT_Campaign_character.kt | 12 +- .../server/rest/campaign/PUT_Campaign_npc.kt | 17 +- .../kotlin/com/pixelized/shared/lwa/Module.kt | 22 +++ .../com/pixelized/shared/lwa/StorePath.kt | 9 + .../shared/lwa/model/AlteredCharacterSheet.kt | 136 ++++++++++++++ .../shared/lwa/model/alteration/Alteration.kt | 19 ++ .../lwa/model/alteration/AlterationJson.kt | 6 + .../model/alteration/AlterationJsonFactory.kt | 73 ++++++++ .../lwa/model/alteration/AlterationJsonV1.kt | 23 +++ .../lwa/model/alteration/FieldAlteration.kt | 9 + .../shared/lwa/model/campaign/Campaign.kt | 17 +- .../lwa/model/campaign/CampaignJsonFactory.kt | 85 ++++++--- .../model/characterSheet/CharacterSheet.kt | 5 - .../CharacterSheetJsonFactory.kt | 22 +-- .../pixelized/shared}/lwa/parser/dice/Dice.kt | 2 +- .../shared}/lwa/parser/dice/DiceParser.kt | 2 +- .../lwa/parser/expression/Expression.kt | 18 +- .../lwa/parser/expression/ExpressionParser.kt | 9 +- .../pixelized/shared}/lwa/parser/word/Word.kt | 2 +- .../shared}/lwa/parser/word/WordParser.kt | 2 +- .../websocket/payload/RestSynchronisation.kt | 7 + .../UpdatePlayerCharacteristicMessage.kt | 6 +- .../lwa/usecase/CharacterSheetUseCase.kt | 20 +- .../shared/lwa/usecase/ExpressionUseCase.kt | 172 ++++++++++++++++++ .../shared/lwa/usecase}/RollUseCase.kt | 4 +- .../shared/lwa/usecase}/SkillStepUseCase.kt | 6 +- 83 files changed, 1797 insertions(+), 811 deletions(-) delete mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/business/ExpressionUseCase.kt delete mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/alteration/model/Alteration.kt delete mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/alteration/model/AlterationMetadata.kt delete mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/alteration/model/FieldAlteration.kt create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/characteristic/CharacterDetailCharacteristicDialogViewModel.kt rename composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/{screen/characterSheet/detail/dialog/CharacterSheetStatDialog.kt => composable/character/characteristic/CharacterSheetCharacteristicDialog.kt} (93%) create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/characteristic/CharacterSheetCharacteristicDialogFactory.kt rename composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/{business => usecase}/SettingsUseCase.kt (89%) create mode 100644 server/src/main/kotlin/com/pixelized/server/lwa/model/alteration/AlterationService.kt create mode 100644 server/src/main/kotlin/com/pixelized/server/lwa/model/alteration/AlterationStore.kt create mode 100644 server/src/main/kotlin/com/pixelized/server/lwa/server/rest/alteration/GET_Active_Alteration.kt create mode 100644 server/src/main/kotlin/com/pixelized/server/lwa/server/rest/alteration/GET_Alteration.kt create mode 100644 server/src/main/kotlin/com/pixelized/server/lwa/server/rest/alteration/PUT_ActiveAlteration.kt create mode 100644 shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/AlteredCharacterSheet.kt create mode 100644 shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/alteration/Alteration.kt create mode 100644 shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/alteration/AlterationJson.kt create mode 100644 shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/alteration/AlterationJsonFactory.kt create mode 100644 shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/alteration/AlterationJsonV1.kt create mode 100644 shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/alteration/FieldAlteration.kt rename {composeApp/src/commonMain/kotlin/com/pixelized/desktop => shared/src/commonMain/kotlin/com/pixelized/shared}/lwa/parser/dice/Dice.kt (91%) rename {composeApp/src/commonMain/kotlin/com/pixelized/desktop => shared/src/commonMain/kotlin/com/pixelized/shared}/lwa/parser/dice/DiceParser.kt (94%) rename {composeApp/src/commonMain/kotlin/com/pixelized/desktop => shared/src/commonMain/kotlin/com/pixelized/shared}/lwa/parser/expression/Expression.kt (82%) rename {composeApp/src/commonMain/kotlin/com/pixelized/desktop => shared/src/commonMain/kotlin/com/pixelized/shared}/lwa/parser/expression/ExpressionParser.kt (97%) rename {composeApp/src/commonMain/kotlin/com/pixelized/desktop => shared/src/commonMain/kotlin/com/pixelized/shared}/lwa/parser/word/Word.kt (89%) rename {composeApp/src/commonMain/kotlin/com/pixelized/desktop => shared/src/commonMain/kotlin/com/pixelized/shared}/lwa/parser/word/WordParser.kt (82%) create mode 100644 shared/src/commonMain/kotlin/com/pixelized/shared/lwa/usecase/ExpressionUseCase.kt rename {composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/business => shared/src/commonMain/kotlin/com/pixelized/shared/lwa/usecase}/RollUseCase.kt (95%) rename {composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/business => shared/src/commonMain/kotlin/com/pixelized/shared/lwa/usecase}/SkillStepUseCase.kt (95%) 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 4fccf22..fae4d5f 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/Module.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/Module.kt @@ -1,14 +1,7 @@ package com.pixelized.desktop.lwa -import com.pixelized.desktop.lwa.business.ExpressionUseCase -import com.pixelized.desktop.lwa.business.RollUseCase -import com.pixelized.desktop.lwa.business.SettingsUseCase -import com.pixelized.desktop.lwa.business.SkillStepUseCase import com.pixelized.desktop.lwa.network.LwaClient import com.pixelized.desktop.lwa.network.LwaClientImpl -import com.pixelized.desktop.lwa.parser.dice.DiceParser -import com.pixelized.desktop.lwa.parser.expression.ExpressionParser -import com.pixelized.desktop.lwa.parser.word.WordParser import com.pixelized.desktop.lwa.repository.alteration.AlterationRepository import com.pixelized.desktop.lwa.repository.alteration.AlterationStore import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository @@ -20,6 +13,8 @@ import com.pixelized.desktop.lwa.repository.roll_history.RollHistoryRepository import com.pixelized.desktop.lwa.repository.settings.SettingsFactory import com.pixelized.desktop.lwa.repository.settings.SettingsRepository 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.player.detail.CharacterDetailFactory import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailViewModel import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDiminishedViewModel @@ -35,8 +30,8 @@ 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.screen.rollhistory.RollHistoryViewModel +import com.pixelized.desktop.lwa.usecase.SettingsUseCase import com.pixelized.shared.lwa.model.campaign.CampaignJsonFactory -import com.pixelized.shared.lwa.usecase.CharacterSheetUseCase import io.ktor.client.HttpClient import io.ktor.client.engine.HttpClientEngine import io.ktor.client.engine.okhttp.OkHttp @@ -50,7 +45,6 @@ import org.koin.dsl.module val appModuleDependencies get() = listOf( - parserDependencies, factoryDependencies, useCaseDependencies, storeDependencies, @@ -107,6 +101,7 @@ val factoryDependencies factoryOf(::CampaignJsonFactory) factoryOf(::PlayerRibbonFactory) factoryOf(::CharacterDetailFactory) + factoryOf(::CharacterSheetCharacteristicDialogFactory) } val viewModelDependencies @@ -120,20 +115,10 @@ val viewModelDependencies viewModelOf(::PlayerRibbonViewModel) viewModelOf(::CharacterDetailViewModel) viewModelOf(::CharacterDiminishedViewModel) - } - -val parserDependencies - get() = module { - factoryOf(::WordParser) - factoryOf(::DiceParser) - factoryOf(::ExpressionParser) + viewModelOf(::CharacterDetailCharacteristicDialogViewModel) } val useCaseDependencies get() = module { - factoryOf(::SkillStepUseCase) - factoryOf(::RollUseCase) - factoryOf(::ExpressionUseCase) factoryOf(::SettingsUseCase) - factoryOf(::CharacterSheetUseCase) } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/business/ExpressionUseCase.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/business/ExpressionUseCase.kt deleted file mode 100644 index d558b32..0000000 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/business/ExpressionUseCase.kt +++ /dev/null @@ -1,130 +0,0 @@ -package com.pixelized.desktop.lwa.business - - -import com.pixelized.desktop.lwa.parser.expression.Expression -import com.pixelized.desktop.lwa.parser.expression.ExpressionParser -import com.pixelized.desktop.lwa.parser.word.Word -import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet -import kotlin.math.max -import kotlin.math.min - -class ExpressionUseCase( - private val expressionParser: ExpressionParser, - private val rollUseCase: RollUseCase, -) { - fun computeSkillValue( - sheet: CharacterSheet, - skill: CharacterSheet.Skill, - alterations: Int, - ): Int { - val context = Context( - sheet = sheet, - skill = skill, - ) - val base: Int = context.evaluate( - expression = skill.base.let(expressionParser::parse), - ).let { - when (skill.occupation) { - true -> max(MIN_OCCUPATION_VALUE, it) - else -> it - } - } - val bonus = context.evaluate( - expression = skill.bonus?.let(expressionParser::parse), - ) - val level = max((skill.level - 1) * 5, 0) - - return max(base + bonus + level + alterations, 0) - } - - fun computeRoll( - sheet: CharacterSheet, - expression: String, - ): Int { - return expressionParser.parse(input = expression)?.let { - computeExpression(sheet = sheet, expression = it) - } ?: 0 - } - - fun computeExpression( - sheet: CharacterSheet, - expression: Expression, - ): Int { - val context = Context( - sheet = sheet, - skill = null, - ) - print("Evaluate:\"$expression\"") - return context.evaluate( - expression = expression, - ).also { println(" > $it") } - } - - private fun Context.evaluate(expression: Expression?): Int { - - return when (expression) { - is Expression.Add -> { - evaluate(expression.first) + evaluate(expression.second) - } - - is Expression.Minus -> { - evaluate(expression.first) - evaluate(expression.second) - } - - is Expression.Div -> { - evaluate(expression.first) / evaluate(expression.second) - } - - is Expression.Prod -> { - evaluate(expression.first) * evaluate(expression.second) - } - - is Expression.Inversion -> { - -evaluate(expression.expression) - } - - is Expression.Maximum -> { - min(evaluate(expression.first), evaluate(expression.second)) - } - - is Expression.Minimum -> { - max(evaluate(expression.first), evaluate(expression.second)) - } - - is Expression.Flat -> { - expression.value - } - - is Expression.DiceExpression -> { - rollUseCase.roll(expression.dice) - } - - is Expression.UrlExpression -> { - 0 // Ignore this case. - } - - is Expression.WordExpression -> when (expression.word.type) { - Word.Type.BDC -> evaluate(expressionParser.parse(sheet.damageBonus)) - Word.Type.BDD -> evaluate(expressionParser.parse(sheet.damageBonus)) - 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 - } - } - - data class Context( - val sheet: CharacterSheet, - val skill: CharacterSheet.Skill?, - ) - - companion object { - private const val MIN_OCCUPATION_VALUE = 40 - } -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClient.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClient.kt index fdc2354..1f2ae80 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClient.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClient.kt @@ -1,5 +1,6 @@ package com.pixelized.desktop.lwa.network +import com.pixelized.shared.lwa.model.alteration.AlterationJson import com.pixelized.shared.lwa.model.campaign.CampaignJson import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJson import com.pixelized.shared.lwa.protocol.rest.CharacterPreviewJson @@ -16,11 +17,21 @@ interface LwaClient { suspend fun campaign(): CampaignJson - suspend fun campaignAddCharacter(id: String) + suspend fun campaignAddCharacter(characterSheetId: String, instanceId: Int) - suspend fun campaignDeleteCharacter(id: String) + suspend fun campaignDeleteCharacter(characterSheetId: String, instanceId: Int) - suspend fun campaignAddNpc(id: String) + suspend fun campaignAddNpc(characterSheetId: String, instanceId: Int) - suspend fun campaignDeleteNpc(id: String) + suspend fun campaignDeleteNpc(characterSheetId: String, instanceId: Int) + + suspend fun alterations(): List + + suspend fun activeAlterations(characterSheetId: String, instanceId: Int): List + + suspend fun toggleActiveAlterations( + characterSheetId: String, + instanceId: Int, + alterationId: String, + ) } \ No newline at end of file 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 5c52c57..8b0d08d 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 @@ -1,6 +1,7 @@ package com.pixelized.desktop.lwa.network import com.pixelized.desktop.lwa.repository.settings.SettingsRepository +import com.pixelized.shared.lwa.model.alteration.AlterationJson import com.pixelized.shared.lwa.model.campaign.CampaignJson import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJson import com.pixelized.shared.lwa.protocol.rest.CharacterPreviewJson @@ -46,19 +47,55 @@ class LwaClientImpl( .get("$root/campaign") .body() - override suspend fun campaignAddCharacter(id: String) = client - .put("$root/campaign/character/update?id=$id") + override suspend fun campaignAddCharacter( + characterSheetId: String, + instanceId: Int, + ) = client + .put("$root/campaign/character/update?characterSheetId=$characterSheetId&instanceId=$instanceId") .body() - override suspend fun campaignDeleteCharacter(id: String) = client - .delete("$root/campaign/character/delete?id=$id") + override suspend fun campaignDeleteCharacter( + characterSheetId: String, + instanceId: Int, + ) = client + .delete("$root/campaign/character/delete?characterSheetId=$characterSheetId&instanceId=$instanceId") .body() - override suspend fun campaignAddNpc(id: String) = client - .put("$root/campaign/npc/update?id=$id") + override suspend fun campaignAddNpc( + characterSheetId: String, + instanceId: Int, + ) = client + .put("$root/campaign/npc/update?characterSheetId=$characterSheetId&instanceId=$instanceId") .body() - override suspend fun campaignDeleteNpc(id: String) = client - .delete("$root/campaign/npc/delete?id=$id") + override suspend fun campaignDeleteNpc( + characterSheetId: String, + instanceId: Int, + ) = client + .delete("$root/campaign/npc/delete?characterSheetId=$characterSheetId&instanceId=$instanceId") + .body() + + override suspend fun alterations(): List = client + .get("$root/alterations") + .body() + + override suspend fun activeAlterations( + characterSheetId: String, + instanceId: Int, + ): List = client + .get("$root/alterations/active?characterSheetId=$characterSheetId&instanceId=$instanceId") + .body() + + override suspend fun toggleActiveAlterations( + characterSheetId: String, + instanceId: Int, + alterationId: String, + ) = client + .put("$root/alterations/active/toggle?characterSheetId=$characterSheetId&instanceId=$instanceId") { + url { + 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 1d1cd9a..ec0ba3c 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 @@ -1,63 +1,85 @@ package com.pixelized.desktop.lwa.repository.alteration -import com.pixelized.desktop.lwa.repository.alteration.model.FieldAlteration +import com.pixelized.shared.lwa.model.alteration.Alteration +import com.pixelized.shared.lwa.model.alteration.FieldAlteration +import com.pixelized.shared.lwa.model.campaign.Campaign +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow +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 // Theses typealias are there for readability only. -private typealias CharacterId = String private typealias AlterationId = String class AlterationRepository( private val store: AlterationStore, ) { - private val activeAlterationIdMapFlow: HashMap>> = - hashMapOf("0f2117e9-e077-4354-8d77-20150df1c462" to MutableStateFlow(listOf("7c00dafa-a67d-4351-8ea9-67d933012cde", "65e37d32-3031-4bf8-9369-d2c45d2efac0"))) + private val scope = CoroutineScope(Dispatchers.IO + Job()) + private val activeAlterationMapFlow: StateFlow>>> = + combine( + store.alterations, + store.active, + ) { alterations, actives -> + actives.map { activeEntry -> + activeEntry.key to transformToAlterationFieldMap( + alterations = alterations, + actives = activeEntry.value + ) + }.toMap() + }.stateIn( + scope = scope, + started = SharingStarted.Eagerly, + initialValue = emptyMap(), + ) - fun alterationsFlow(characterId: String): Flow>> { - return activeAlterationIdMapFlow - .getOrPut(characterId) { MutableStateFlow(emptyList()) } - .map { activeAlterationIds -> - val fieldAlterations = hashMapOf>() - - activeAlterationIds.forEach { id: AlterationId -> - store.alteration(alterationId = id)?.let { alteration -> - alteration.fields.forEach { field -> - fieldAlterations.getOrPut(field.fieldId) { mutableListOf() } - .add( - FieldAlteration( - alterationId = alteration.id, - metadata = alteration.metadata, - expression = field.expression, - ) - ) - } - } - } - - fieldAlterations - } + fun alterationsFlow( + characterId: Campaign.CharacterInstance.Id, + ): Flow>> { + return activeAlterationMapFlow.map { it[characterId] ?: emptyMap() } } - fun toggle(characterId: String, alterationId: String) { + fun alterations( + characterInstanceId: Campaign.CharacterInstance.Id, + ): Map> { + return activeAlterationMapFlow.value[characterInstanceId] ?: emptyMap() + } - // check if the alteration is currently active of inactive. - val active = activeAlterationIdMapFlow[characterId] - ?.value - ?.contains(alterationId) - ?: false - - // alteration was active for the character toggle it off. - activeAlterationIdMapFlow[characterId]?.value = activeAlterationIdMapFlow[characterId] - ?.value - ?.toMutableList() - ?.also { list -> - when (active) { - true -> list.remove(alterationId) - else -> list.add(alterationId) + private fun transformToAlterationFieldMap( + alterations: Map, + actives: List, + ): Map> { + val fieldAlterations = hashMapOf>() + actives.forEach { id: AlterationId -> + alterations[id]?.let { alteration -> + alteration.fields.forEach { field -> + fieldAlterations + .getOrPut(field.fieldId) { mutableListOf() } + .add( + FieldAlteration( + alterationId = alteration.id, + metadata = alteration.metadata, + expression = field.expression, + ) + ) } } - ?: emptyList() + } + return fieldAlterations + } + + suspend fun toggleActiveAlteration( + characterInstanceId: Campaign.CharacterInstance.Id, + alterationId: String, + ) { + // alteration was active for the character toggle it off. + store.toggleActiveAlteration( + characterInstance = characterInstanceId, + alterationId = alterationId, + ) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/alteration/AlterationStore.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/alteration/AlterationStore.kt index 81b9d11..c367607 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/alteration/AlterationStore.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/alteration/AlterationStore.kt @@ -1,81 +1,114 @@ package com.pixelized.desktop.lwa.repository.alteration -import com.pixelized.desktop.lwa.parser.expression.Expression -import com.pixelized.desktop.lwa.parser.expression.ExpressionParser -import com.pixelized.desktop.lwa.repository.alteration.model.Alteration -import com.pixelized.desktop.lwa.repository.alteration.model.AlterationMetadata -import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId -import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.ARMOR -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.MOV -import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.STR -import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CommonSkillId.ACROBATICS_ID -import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CommonSkillId.AID_ID -import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CommonSkillId.ATHLETICS_ID -import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CommonSkillId.BARGAIN_ID -import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CommonSkillId.COMBAT_ID -import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CommonSkillId.DISCRETION_ID -import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CommonSkillId.INTIMIDATION_ID -import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CommonSkillId.PERCEPTION_ID -import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CommonSkillId.PERSUASION_ID -import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CommonSkillId.SLEIGHT_OF_HAND_ID -import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CommonSkillId.SPIEL_ID -import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CommonSkillId.THROW_ID +import com.pixelized.desktop.lwa.network.LwaClient +import com.pixelized.desktop.lwa.repository.network.NetworkRepository +import com.pixelized.shared.lwa.model.alteration.Alteration +import com.pixelized.shared.lwa.model.alteration.AlterationJsonFactory +import com.pixelized.shared.lwa.model.campaign.Campaign.CharacterInstance +import com.pixelized.shared.lwa.model.campaign.CampaignJsonFactory +import com.pixelized.shared.lwa.protocol.websocket.Message +import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch class AlterationStore( - private val expressionParser: ExpressionParser, + private val alterationFactory: AlterationJsonFactory, + private val campaignJsonFactory: CampaignJsonFactory, + private val network: NetworkRepository, + private val client: LwaClient, ) { - private val alterations = mapOf( - "7c00dafa-a67d-4351-8ea9-67d933012cde" to Alteration( - id = "7c00dafa-a67d-4351-8ea9-67d933012cde", - metadata = AlterationMetadata( - name = "Tatouage Mak", - description = "Tatouage des Mak permettant la transformation en loup.", - ), - fields = listOf( - Alteration.Field(fieldId = CharacteristicId.PP, expression = "-2".parse()), - ) - ), - "65e37d32-3031-4bf8-9369-d2c45d2efac0" to Alteration( - id = "65e37d32-3031-4bf8-9369-d2c45d2efac0", - metadata = AlterationMetadata( - name = "Forme de loup", - description = "Capacité spécial des maks de la tribue Palok.", - ), - fields = listOf( - Alteration.Field(fieldId = STR, expression = "+1".parse()), - Alteration.Field(fieldId = DEX, expression = "-1".parse()), - Alteration.Field(fieldId = HEI, expression = "-1".parse()), - Alteration.Field(fieldId = MOV, expression = "+5".parse()), - Alteration.Field(fieldId = ARMOR, expression = "+1".parse()), + private val _alterations = MutableStateFlow>(emptyMap()) + val alterations: StateFlow> = _alterations - Alteration.Field(fieldId = COMBAT_ID, expression = "+10".parse()), - Alteration.Field(fieldId = THROW_ID, expression = "-100".parse()), - Alteration.Field(fieldId = ATHLETICS_ID, expression = "+20".parse()), - Alteration.Field(fieldId = ACROBATICS_ID, expression = "-10".parse()), - Alteration.Field(fieldId = PERCEPTION_ID, expression = "+20".parse()), - Alteration.Field(fieldId = PERSUASION_ID, expression = "-20".parse()), - Alteration.Field(fieldId = INTIMIDATION_ID, expression = "+20".parse()), - Alteration.Field(fieldId = SPIEL_ID, expression = "-20".parse()), - Alteration.Field(fieldId = BARGAIN_ID, expression = "-20".parse()), - Alteration.Field(fieldId = DISCRETION_ID, expression = "+20".parse()), - Alteration.Field(fieldId = SLEIGHT_OF_HAND_ID, expression = "-100".parse()), - Alteration.Field(fieldId = AID_ID, expression = "-100".parse()), + private val _active = MutableStateFlow>>(emptyMap()) + val active: StateFlow>> get() = _active - Alteration.Field( - fieldId = "40a4dcca-7010-4522-9d58-0cfac0a586e8", // Pistage - expression = "+20".parse() - ), - ) + init { + val scope = CoroutineScope(Dispatchers.IO + Job()) + scope.launch { + updateAlterations() + } + scope.launch { + network.data.collect(::handleMessage) + } + } + + private suspend fun updateAlterations() { + _alterations.value = loadAlteration() + } + + private suspend fun loadAlteration(): Map { + val request = client.alterations() + val data = request.map { alterationFactory.convertFromJson(json = it) } + return data.associateBy { it.id } + } + + private suspend fun loadActiveAlterations( + characterInstanceId: CharacterInstance.Id, + ): List { + val request = client.activeAlterations( + characterSheetId = characterInstanceId.characterSheetId, + instanceId = characterInstanceId.instanceId, ) - ) + _active.value = _active.value.toMutableMap().also { + it[characterInstanceId] = request + } + return request + } - fun alterations(): Collection = alterations.values + fun alterations(): Collection { + return alterations.value.values + } - fun alteration(alterationId: String): Alteration? = alterations[alterationId] + fun alteration(alterationId: String): Alteration? { + return alterations.value[alterationId] + } - private fun String.parse(): Expression { - return expressionParser.parse(this)!! + suspend fun toggleActiveAlteration( + characterInstance: CharacterInstance.Id, + alterationId: String, + ) { + client.toggleActiveAlterations( + characterSheetId = characterInstance.characterSheetId, + instanceId = characterInstance.instanceId, + alterationId = alterationId, + ) + } + + private suspend fun handleMessage(message: Message) { + when (val payload = message.value) { + is RestSynchronisation.ToggleActiveAlteration -> { + setActiveAlteration( + characterInstanceId = campaignJsonFactory.convertFromV1( + characterInstanceIdJson = payload.characterId, + ), + alterationId = payload.alterationId, + active = payload.active, + ) + } + + else -> Unit + } + } + + private suspend fun setActiveAlteration( + characterInstanceId: CharacterInstance.Id, + alterationId: String, + active: Boolean, + ) { + _active.value = _active.value.toMutableMap().also { map -> + map[characterInstanceId] = map[characterInstanceId]?.toMutableList() + ?.also { + when { + it.contains(alterationId) && !active -> it.remove(alterationId) + !it.contains(alterationId) && active -> it.add(alterationId) + } + } + ?: listOfNotNull(if (active) alterationId else null) + } } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/alteration/model/Alteration.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/alteration/model/Alteration.kt deleted file mode 100644 index ba0cf75..0000000 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/alteration/model/Alteration.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.pixelized.desktop.lwa.repository.alteration.model - -import com.pixelized.desktop.lwa.parser.expression.Expression - -data class Alteration( - val id: String, - val metadata: AlterationMetadata, - val fields: List, -) { - data class Field( - val fieldId: String, // this id is not the id of the instance but the id of the impacted characteristic in the character sheet. - val expression: Expression, - ) -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/alteration/model/AlterationMetadata.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/alteration/model/AlterationMetadata.kt deleted file mode 100644 index d4e0640..0000000 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/alteration/model/AlterationMetadata.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.pixelized.desktop.lwa.repository.alteration.model - -data class AlterationMetadata( - val name: String, - val description: String, -) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/alteration/model/FieldAlteration.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/alteration/model/FieldAlteration.kt deleted file mode 100644 index c6cfe6d..0000000 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/alteration/model/FieldAlteration.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.pixelized.desktop.lwa.repository.alteration.model - -import com.pixelized.desktop.lwa.parser.expression.Expression - -data class FieldAlteration( - val alterationId: String, - val metadata: AlterationMetadata, - val expression: Expression, -) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/campaign/CampaignRepository.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/campaign/CampaignRepository.kt index 2168f5d..7cd1b59 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/campaign/CampaignRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/campaign/CampaignRepository.kt @@ -17,7 +17,9 @@ class CampaignRepository( val campaignFlow get() = store.campaignFlow - fun characterInstanceFlow(id: String): StateFlow { + fun characterInstanceFlow( + id: Campaign.CharacterInstance.Id, + ): StateFlow { return campaignFlow .mapNotNull { it.characters[id] @@ -28,4 +30,10 @@ class CampaignRepository( initialValue = campaignFlow.value.character(id = id), ) } + + fun characterInstance( + chracterInstanceId: Campaign.CharacterInstance.Id, + ): Campaign.CharacterInstance { + return campaignFlow.value.character(chracterInstanceId) + } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/campaign/CampaignStore.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/campaign/CampaignStore.kt index ee72d71..ed78592 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/campaign/CampaignStore.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/campaign/CampaignStore.kt @@ -47,7 +47,7 @@ class CampaignStore( } private fun updateCharacteristic( - characterId: String, + characterId: Campaign.CharacterInstance.Id, characteristic: Campaign.CharacterInstance.Characteristic, value: Int, ) { @@ -69,9 +69,11 @@ class CampaignStore( } is UpdatePlayerCharacteristicMessage -> { + val id = factory.convertFromV1(characterInstanceIdJson = payload.characterInstanceId) + val characteristic = factory.convertFromV1(characteristicJson = payload.characteristic) updateCharacteristic( - characterId = payload.characterId, - characteristic = payload.characteristic, + characterId = id, + characteristic = characteristic, value = payload.value, ) } 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 fcae482..0476594 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 @@ -23,11 +23,11 @@ class CharacterSheetRepository( } suspend fun characterDetail( - characterId: String?, + characterSheetId: String?, forceUpdate: Boolean = false, ): CharacterSheet? { return try { - characterId?.let { store.characterDetail(characterId = it, forceUpdate = forceUpdate) } + characterSheetId?.let { store.characterDetail(characterId = it, forceUpdate = forceUpdate) } } catch (exception: Exception) { null } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/settings/SettingsFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/settings/SettingsFactory.kt index e6eb986..188faa6 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/settings/SettingsFactory.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/settings/SettingsFactory.kt @@ -1,6 +1,6 @@ package com.pixelized.desktop.lwa.repository.settings -import com.pixelized.desktop.lwa.business.SettingsUseCase +import com.pixelized.desktop.lwa.usecase.SettingsUseCase import com.pixelized.desktop.lwa.repository.settings.model.Settings import com.pixelized.desktop.lwa.repository.settings.model.SettingsJson import com.pixelized.desktop.lwa.repository.settings.model.SettingsJsonV1 diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/settings/SettingsRepository.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/settings/SettingsRepository.kt index aa4e04c..f3344ca 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/settings/SettingsRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/settings/SettingsRepository.kt @@ -1,6 +1,6 @@ package com.pixelized.desktop.lwa.repository.settings -import com.pixelized.desktop.lwa.business.SettingsUseCase +import com.pixelized.desktop.lwa.usecase.SettingsUseCase import com.pixelized.desktop.lwa.repository.settings.model.Settings import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/settings/SettingsStore.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/settings/SettingsStore.kt index d2b6cfb..094f820 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/settings/SettingsStore.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/settings/SettingsStore.kt @@ -1,6 +1,6 @@ package com.pixelized.desktop.lwa.repository.settings -import com.pixelized.desktop.lwa.business.SettingsUseCase +import com.pixelized.desktop.lwa.usecase.SettingsUseCase import com.pixelized.desktop.lwa.repository.settings.model.Settings import com.pixelized.desktop.lwa.repository.settings.model.SettingsJson import com.pixelized.shared.lwa.storePath @@ -10,7 +10,6 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch -import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import java.io.File diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/characteristic/CharacterDetailCharacteristicDialogViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/characteristic/CharacterDetailCharacteristicDialogViewModel.kt new file mode 100644 index 0000000..54834de --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/characteristic/CharacterDetailCharacteristicDialogViewModel.kt @@ -0,0 +1,100 @@ +package com.pixelized.desktop.lwa.ui.composable.character.characteristic + +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.campaign.CampaignRepository +import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository +import com.pixelized.desktop.lwa.repository.network.NetworkRepository +import com.pixelized.shared.lwa.model.AlteredCharacterSheetFactory +import com.pixelized.shared.lwa.model.alteration.FieldAlteration +import com.pixelized.shared.lwa.model.campaign.Campaign +import com.pixelized.shared.lwa.model.campaign.Campaign.CharacterInstance.Characteristic +import com.pixelized.shared.lwa.model.campaign.CampaignJsonFactory +import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet +import com.pixelized.shared.lwa.protocol.websocket.payload.UpdatePlayerCharacteristicMessage + +class CharacterDetailCharacteristicDialogViewModel( + private val characterSheetRepository: CharacterSheetRepository, + private val campaignRepository: CampaignRepository, + private val alterationRepository: AlterationRepository, + private val campaignJsonFactory: CampaignJsonFactory, + private val alteredCharacterSheetFactory: AlteredCharacterSheetFactory, + private val factory: CharacterSheetCharacteristicDialogFactory, + private val network: NetworkRepository, +) : ViewModel() { + + private val _statChangeDialog = mutableStateOf(null) + val statChangeDialog: State get() = _statChangeDialog + + fun hideSubCharacteristicDialog() { + _statChangeDialog.value = null + } + + suspend fun showSubCharacteristicDialog( + characterInstanceId: Campaign.CharacterInstance.Id, + characteristic: Characteristic, + ) { + val sheet: CharacterSheet? = characterSheetRepository.characterDetail( + characterSheetId = characterInstanceId.characterSheetId, + ) + val characterInstance: Campaign.CharacterInstance = campaignRepository.characterInstance( + chracterInstanceId = characterInstanceId, + ) + val alterations: Map> = alterationRepository.alterations( + characterInstanceId = characterInstanceId, + ) + _statChangeDialog.value = factory.convertToDialogUio( + characterInstanceId = characterInstanceId, + characteristic = characteristic, + characterSheet = sheet, + characterInstance = characterInstance, + alterations = alterations, + ) + } + + suspend fun changeSubCharacteristic( + characterInstanceId: Campaign.CharacterInstance.Id, + characteristic: Characteristic, + value: Int, + ) { + // fetch the linked character sheet + val sheet = characterSheetRepository.characterDetail( + characterSheetId = characterInstanceId.characterSheetId, + ) + val alterations = alterationRepository.alterations( + characterInstanceId = characterInstanceId, + ) + // we need the maximum HP / Power that the character sheet have. + if (sheet != null) { + val alteredSheet = alteredCharacterSheetFactory.sheet( + characterSheet = sheet, + alterations = alterations, + ) + // convert the data to json format + val characterInstanceIdJson = campaignJsonFactory.convertToJson( + id = characterInstanceId, + ) + val characteristicJson = campaignJsonFactory.convertToJson( + characteristic = characteristic, + ) + // share the data through the websocket. + network.share( + payload = UpdatePlayerCharacteristicMessage( + characterInstanceId = characterInstanceIdJson, + characteristic = characteristicJson, + value = when (characteristic) { + Characteristic.Damage -> { + alteredSheet.maxHp - value + } + + Characteristic.Power -> { + alteredSheet.maxPp - value + } + }, + ), + ) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/characterSheet/detail/dialog/CharacterSheetStatDialog.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/characteristic/CharacterSheetCharacteristicDialog.kt similarity index 93% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/characterSheet/detail/dialog/CharacterSheetStatDialog.kt rename to composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/characteristic/CharacterSheetCharacteristicDialog.kt index fe61236..8216c67 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/characterSheet/detail/dialog/CharacterSheetStatDialog.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/characteristic/CharacterSheetCharacteristicDialog.kt @@ -1,4 +1,4 @@ -package com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.dialog +package com.pixelized.desktop.lwa.ui.composable.character.characteristic import androidx.compose.animation.AnimatedContent import androidx.compose.animation.SizeTransform @@ -49,8 +49,9 @@ import lwacharactersheet.composeapp.generated.resources.dialog__confirm_action import org.jetbrains.compose.resources.stringResource @Stable -data class StatChangeDialogUio( - val id: Campaign.CharacterInstance.Characteristic, +data class CharacterSheetCharacteristicDialogUio( + val characterInstanceId: Campaign.CharacterInstance.Id, + val characteristic: Campaign.CharacterInstance.Characteristic, val label: String, val value: () -> TextFieldValue, val onValueChange: (TextFieldValue) -> Unit, @@ -58,9 +59,9 @@ data class StatChangeDialogUio( ) @Composable -fun CharacterSheetStatDialog( - dialog: State, - onConfirm: (StatChangeDialogUio) -> Unit, +fun CharacterSheetCharacteristicDialog( + dialog: State, + onConfirm: (CharacterSheetCharacteristicDialogUio) -> Unit, onDismissRequest: () -> Unit, ) { AnimatedContent( @@ -92,8 +93,8 @@ fun CharacterSheetStatDialog( @Composable private fun Dialog( - dialog: StatChangeDialogUio, - onConfirm: (StatChangeDialogUio) -> Unit, + dialog: CharacterSheetCharacteristicDialogUio, + onConfirm: (CharacterSheetCharacteristicDialogUio) -> Unit, onDismissRequest: () -> Unit, ) { val typography = MaterialTheme.typography diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/characteristic/CharacterSheetCharacteristicDialogFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/characteristic/CharacterSheetCharacteristicDialogFactory.kt new file mode 100644 index 0000000..638ccd0 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/character/characteristic/CharacterSheetCharacteristicDialogFactory.kt @@ -0,0 +1,70 @@ +package com.pixelized.desktop.lwa.ui.composable.character.characteristic + +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.TextFieldValue +import com.pixelized.shared.lwa.model.AlteredCharacterSheetFactory +import com.pixelized.shared.lwa.model.alteration.FieldAlteration +import com.pixelized.shared.lwa.model.campaign.Campaign +import com.pixelized.shared.lwa.model.campaign.Campaign.CharacterInstance.Characteristic +import com.pixelized.shared.lwa.model.campaign.damage +import com.pixelized.shared.lwa.model.campaign.power +import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet +import lwacharactersheet.composeapp.generated.resources.Res +import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__sub_characteristics__hit_point +import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__sub_characteristics__power_point +import org.jetbrains.compose.resources.getString + +class CharacterSheetCharacteristicDialogFactory( + private val alteredCharacterSheetFactory: AlteredCharacterSheetFactory, +) { + + suspend fun convertToDialogUio( + characterInstanceId: Campaign.CharacterInstance.Id, + characteristic: Characteristic, + characterSheet: CharacterSheet?, + characterInstance: Campaign.CharacterInstance, + alterations: Map>, + ): CharacterSheetCharacteristicDialogUio? { + if (characterSheet == null) return null + + val alteredCharacterSheet = alteredCharacterSheetFactory.sheet( + characterSheet = characterSheet, + alterations = alterations, + ) + + return when (characteristic) { + Characteristic.Damage -> { + val value = mutableStateOf( + "${alteredCharacterSheet.maxHp - characterInstance.damage}".let { + TextFieldValue(text = it, selection = TextRange(it.length)) + } + ) + CharacterSheetCharacteristicDialogUio( + characterInstanceId = characterInstanceId, + characteristic = characteristic, + label = getString(resource = Res.string.character_sheet_edit__sub_characteristics__hit_point), + value = { value.value }, + onValueChange = { value.value = it }, + maxValue = "${alteredCharacterSheet.maxHp}", + ) + } + + Characteristic.Power -> { + val value = mutableStateOf( + "${alteredCharacterSheet.maxPp - characterInstance.power}".let { + TextFieldValue(text = it, selection = TextRange(it.length)) + } + ) + CharacterSheetCharacteristicDialogUio( + characterInstanceId = characterInstanceId, + characteristic = characteristic, + label = getString(resource = Res.string.character_sheet_edit__sub_characteristics__power_point), + value = { value.value }, + onValueChange = { value.value = it }, + maxValue = "${alteredCharacterSheet.maxPp}", + ) + } + } + } +} \ 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 795dbb1..ef5442a 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 @@ -3,30 +3,43 @@ 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.characterSheet.detail.CharacterSheetPage import com.pixelized.desktop.lwa.utils.extention.ARG +import com.pixelized.shared.lwa.model.campaign.Campaign object CharacterSheetDestination { private const val ROUTE = "character.sheet" - private const val CHARACTER_ID = "id" + private const val CHARACTER_SHEET_ID = "sheetId" + private const val CHARACTER_INSTANCE_ID = "instanceId" - fun baseRoute() = "$ROUTE?${CHARACTER_ID.ARG}" + fun baseRoute() = "$ROUTE?${CHARACTER_SHEET_ID.ARG}&${CHARACTER_INSTANCE_ID.ARG}" - fun navigationRoute(id: String) = "$ROUTE?$CHARACTER_ID=$id" + fun navigationRoute(id: Campaign.CharacterInstance.Id) = ROUTE + + "?$CHARACTER_SHEET_ID=${id.characterSheetId}" + + "&$CHARACTER_INSTANCE_ID=${id.instanceId}" fun arguments() = listOf( - navArgument(CHARACTER_ID) { - nullable = true - } + navArgument(CHARACTER_SHEET_ID) { + nullable = false + type = NavType.StringType + }, + navArgument(CHARACTER_INSTANCE_ID) { + nullable = false + type = NavType.IntType + }, ) data class Argument( - val id: String, + val characterInstanceId: Campaign.CharacterInstance.Id, ) { constructor(savedStateHandle: SavedStateHandle) : this( - id = savedStateHandle.get(CHARACTER_ID) ?: error("missing character id") + characterInstanceId = Campaign.CharacterInstance.Id( + savedStateHandle.get(CHARACTER_SHEET_ID) ?: error("missing character id"), + savedStateHandle.get(CHARACTER_INSTANCE_ID) ?: error("missing character id"), + ), ) } } @@ -41,7 +54,7 @@ fun NavGraphBuilder.composableCharacterSheetPage() { } fun NavHostController.navigateToCharacterSheet( - id: String, + id: Campaign.CharacterInstance.Id, ) { val route = CharacterSheetDestination.navigationRoute(id = id) navigate(route = route) diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/window/destination/CharacterSheetWindow.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/window/destination/CharacterSheetWindow.kt index fa17c06..3151ed3 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/window/destination/CharacterSheetWindow.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/window/destination/CharacterSheetWindow.kt @@ -4,10 +4,11 @@ import androidx.compose.runtime.Stable import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import com.pixelized.desktop.lwa.ui.navigation.window.WindowController +import com.pixelized.shared.lwa.model.campaign.Campaign @Stable class CharacterSheetWindow( - val characterId: String, + val characterId: Campaign.CharacterInstance.Id, title: String, size: DpSize, ) : Window( @@ -16,7 +17,7 @@ class CharacterSheetWindow( ) fun WindowController.navigateToCharacterSheet( - characterId: String, + characterId: Campaign.CharacterInstance.Id, title: String, ) { showWindow( 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 1fbfa5e..74120dd 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 @@ -94,7 +94,7 @@ fun CampaignScreen( modifier = Modifier .padding(all = 8.dp) .fillMaxHeight(), - viewModel = characterDetailViewModel, + detailViewModel = characterDetailViewModel, dismissedViewModel = dismissedViewModel, ) }, 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 879c63b..76099d8 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDetail.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDetail.kt @@ -1,6 +1,8 @@ package com.pixelized.desktop.lwa.ui.screen.campaign.player.detail import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInHorizontally @@ -24,8 +26,8 @@ import androidx.compose.material.Surface import androidx.compose.material.Text 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.Alignment import androidx.compose.ui.Modifier @@ -33,7 +35,11 @@ import androidx.compose.ui.draw.clip 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.blur.BlurContentController +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.theme.lwa +import com.pixelized.shared.lwa.model.campaign.Campaign import kotlinx.coroutines.launch import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.ic_close_24dp @@ -46,34 +52,36 @@ import org.koin.compose.viewmodel.koinViewModel @Stable data class CharacterDetailHeaderUio( - val id: String, + val id: Campaign.CharacterInstance.Id, val portrait: String?, val name: String, val hp: String, + val maxHp: String, val pp: String, + val maxPp: String, val mov: String, ) -@Stable -data class CharacterDetailHeaderInstanceUio( - val hp: String, - val pp: String, -) - @Composable fun CharacterDetail( modifier: Modifier = Modifier, dismissedViewModel: CharacterDiminishedViewModel, - viewModel: CharacterDetailViewModel = koinViewModel(), + detailViewModel: CharacterDetailViewModel = koinViewModel(), + characteristicDialogViewModel: CharacterDetailCharacteristicDialogViewModel = koinViewModel(), ) { + val blurController = remember { BlurContentController() } val scope = rememberCoroutineScope() - val detail = viewModel.detail.collectAsState() + val detail = detailViewModel.detail.collectAsState() AnimatedContent( modifier = modifier, targetState = detail.value, transitionSpec = { - (fadeIn() + slideInHorizontally { it / 2 }).togetherWith(fadeOut()) + if (initialState?.id != targetState?.id) { + (fadeIn() + slideInHorizontally { it / 2 }).togetherWith(fadeOut()) + } else { + EnterTransition.None togetherWith ExitTransition.None + } } ) { when (it) { @@ -82,25 +90,58 @@ fun CharacterDetail( ) else -> { - val dynDetail = viewModel.collectDynamicDetailAsState(id = it.id) - - CharacterDetailContent( + Box( modifier = Modifier .fillMaxHeight() .width(width = 128.dp * 4), - character = it, - dynDetail = dynDetail, - onDismissRequest = { - viewModel.hideCharacter() - }, - onDiminished = { - scope.launch { - dismissedViewModel.showDiminishedDialog(id = it.id) + ) { + CharacterDetailContent( + modifier = Modifier.matchParentSize(), + character = it, + onDismissRequest = { + detailViewModel.hideCharacter() + }, + onDiminished = { + scope.launch { + dismissedViewModel.showDiminishedDialog(id = it.id) + } + }, + onHp = { + scope.launch { + characteristicDialogViewModel.showSubCharacteristicDialog( + characterInstanceId = it.id, + characteristic = Campaign.CharacterInstance.Characteristic.Damage, + ) + } + }, + onPp = { + scope.launch { + characteristicDialogViewModel.showSubCharacteristicDialog( + characterInstanceId = it.id, + characteristic = Campaign.CharacterInstance.Characteristic.Power, + ) + } + }, + ) + CharacterSheetCharacteristicDialog( + dialog = characteristicDialogViewModel.statChangeDialog, + onConfirm = { dialog -> + scope.launch { + characteristicDialogViewModel.changeSubCharacteristic( + characterInstanceId = dialog.characterInstanceId, + characteristic = dialog.characteristic, + value = dialog.value().text.toIntOrNull() ?: 0, + ) + characteristicDialogViewModel.hideSubCharacteristicDialog() + blurController.hide() + } + }, + onDismissRequest = { + characteristicDialogViewModel.hideSubCharacteristicDialog() + blurController.hide() } - }, - onHp = { }, - onPp = { }, - ) + ) + } } } } @@ -110,7 +151,6 @@ fun CharacterDetail( fun CharacterDetailContent( modifier: Modifier = Modifier, character: CharacterDetailHeaderUio, - dynDetail: State, onDismissRequest: () -> Unit, onDiminished: () -> Unit, onHp: () -> Unit, @@ -127,7 +167,6 @@ fun CharacterDetailContent( modifier = Modifier.padding(start = 16.dp).fillMaxWidth(), character = character, onDismissRequest = onDismissRequest, - dynDetail = dynDetail, onDiminished = onDiminished, onHp = onHp, onPp = onPp, @@ -169,7 +208,6 @@ private fun Background( private fun CharacterHeader( modifier: Modifier = Modifier, character: CharacterDetailHeaderUio, - dynDetail: State, onDismissRequest: () -> Unit, onDiminished: () -> Unit, onHp: () -> Unit, @@ -224,13 +262,13 @@ private fun CharacterHeader( style = MaterialTheme.typography.h6, color = MaterialTheme.lwa.colorScheme.base.primary, fontWeight = FontWeight.Bold, - text = dynDetail.value?.hp ?: character.hp, + text = character.hp, ) Text( modifier = Modifier.alignByBaseline(), style = MaterialTheme.typography.caption, fontWeight = FontWeight.Thin, - text = "/${character.hp}", + text = "/${character.maxHp}", ) } Row( @@ -247,13 +285,13 @@ private fun CharacterHeader( style = MaterialTheme.typography.h6, color = MaterialTheme.lwa.colorScheme.base.primary, fontWeight = FontWeight.Bold, - text = dynDetail.value?.pp ?: character.pp, + text = character.pp, ) Text( modifier = Modifier.alignByBaseline(), style = MaterialTheme.typography.caption, fontWeight = FontWeight.Thin, - text = "/${character.pp}", + text = "/${character.maxPp}", ) } Row( 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 f35e4c3..a29ad35 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,39 +1,41 @@ package com.pixelized.desktop.lwa.ui.screen.campaign.player.detail -import com.pixelized.desktop.lwa.business.ExpressionUseCase -import com.pixelized.desktop.lwa.parser.expression.Expression -import com.pixelized.desktop.lwa.repository.alteration.model.FieldAlteration +import com.pixelized.shared.lwa.model.AlteredCharacterSheetFactory +import com.pixelized.shared.lwa.model.alteration.FieldAlteration +import com.pixelized.shared.lwa.model.campaign.Campaign +import com.pixelized.shared.lwa.model.campaign.damage +import com.pixelized.shared.lwa.model.campaign.power import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet -import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.HP -import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.MOV -import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.PORTRAIT -import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.PP class CharacterDetailFactory( - private val expressionUseCase: ExpressionUseCase, + private val alteredCharacterSheetFactory: AlteredCharacterSheetFactory, ) { fun convertToCharacterDetailHeaderUio( - sheet: CharacterSheet?, + characterInstanceId: Campaign.CharacterInstance.Id, + characterSheet: CharacterSheet?, + characterInstance: Campaign.CharacterInstance, alterations: Map>, ): CharacterDetailHeaderUio? { - if (sheet == null) return null + if (characterSheet == null) return null - fun List?.sum(): Int { - return this?.sumOf { - expressionUseCase.computeExpression(sheet = sheet, expression = it.expression) - } ?: 0 - } + val alteredCharacterSheet = alteredCharacterSheetFactory.sheet( + characterSheet = characterSheet, + alterations = alterations, + ) + + val maxHp = alteredCharacterSheet.maxHp + val maxPp = alteredCharacterSheet.maxPp return CharacterDetailHeaderUio( - id = sheet.id, - portrait = alterations[PORTRAIT] - ?.firstNotNullOfOrNull { (it.expression as? Expression.UrlExpression)?.url } - ?: sheet.portrait, - name = sheet.name, - hp = "${sheet.hp + alterations[HP].sum()}", - pp = "${sheet.pp + alterations[PP].sum()}", - mov = "${sheet.movement + alterations[MOV].sum()}" + id = characterInstanceId, + portrait = alteredCharacterSheet.portrait, + name = alteredCharacterSheet.name, + hp = "${maxHp - characterInstance.damage}", + maxHp = "$maxHp", + pp = "${maxPp - characterInstance.power}", + maxPp = "$maxPp", + mov = "${alteredCharacterSheet.movement}" ) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDetailViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDetailViewModel.kt index fd4520d..a7edc53 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 @@ -1,85 +1,58 @@ package com.pixelized.desktop.lwa.ui.screen.campaign.player.detail -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.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.pixelized.desktop.lwa.repository.alteration.AlterationRepository import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository -import com.pixelized.shared.lwa.model.campaign.damage -import com.pixelized.shared.lwa.model.campaign.power +import com.pixelized.shared.lwa.model.campaign.Campaign import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.flow.flatMap -import kotlinx.coroutines.flow.flatMapConcat import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.stateIn class CharacterDetailViewModel( - private val characterRepository: CharacterSheetRepository, + private val characterSheetRepository: CharacterSheetRepository, private val campaignRepository: CampaignRepository, private val alterationRepository: AlterationRepository, private val characterDetailFactory: CharacterDetailFactory, ) : ViewModel() { - private val displayedCharacterId = MutableStateFlow(null) + private val displayedCharacterId = MutableStateFlow(null) @OptIn(ExperimentalCoroutinesApi::class) - val detail: StateFlow = displayedCharacterId.flatMapLatest { id -> - if (id != null) { - combine( - characterRepository.characterDetailFlow(characterId = id), - alterationRepository.alterationsFlow(characterId = id), - ) { sheet, alteration -> - characterDetailFactory.convertToCharacterDetailHeaderUio( - sheet = sheet, - alterations = alteration, - ) + val detail: StateFlow = displayedCharacterId + .flatMapLatest { characterInstanceId -> + if (characterInstanceId != null) { + campaignRepository + .characterInstanceFlow(id = characterInstanceId) + .flatMapLatest { characterInstance -> + combine( + characterSheetRepository.characterDetailFlow(characterId = characterInstanceId.characterSheetId), + alterationRepository.alterationsFlow(characterId = characterInstanceId), + ) { characterSheet, alterations -> + characterDetailFactory.convertToCharacterDetailHeaderUio( + characterInstanceId = characterInstanceId, + characterSheet = characterSheet, + characterInstance = characterInstance, + alterations = alterations, + ) + } + } + } else { + flowOf(null) } - } else { - flowOf(null) - } - }.stateIn( - scope = viewModelScope, - started = SharingStarted.Eagerly, - initialValue = null, - ) + }.stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = null, + ) - @Composable - @Stable - fun collectDynamicDetailAsState(id: String): State { - val scope = rememberCoroutineScope() - val flow: StateFlow = remember(id) { - combine( - characterRepository.characterDetailFlow(id), - campaignRepository.characterInstanceFlow(id = id), - ) { sheet, instance -> - if (sheet == null) return@combine null - CharacterDetailHeaderInstanceUio( - hp = "${sheet.hp - instance.damage}", - pp = "${sheet.power - instance.power}", - ) - }.stateIn( - scope = scope, - started = SharingStarted.Eagerly, - initialValue = null, - ) - } - return flow.collectAsState() - } - - fun showCharacter(id: String) { + fun showCharacter(id: Campaign.CharacterInstance.Id) { displayedCharacterId.value = id } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDiminishedViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDiminishedViewModel.kt index 92a0056..748a436 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDiminishedViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/detail/CharacterDiminishedViewModel.kt @@ -7,18 +7,21 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.ViewModel import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.dialog.DiminishedStatDialogUio +import com.pixelized.shared.lwa.model.campaign.Campaign import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.character_sheet__diminished__label import org.jetbrains.compose.resources.getString class CharacterDiminishedViewModel( - private val repository: CharacterSheetRepository, + private val characterSheetRepository: CharacterSheetRepository, ) : ViewModel() { private val _diminishedDialog = mutableStateOf(null) val diminishedDialog: State get() = _diminishedDialog - suspend fun showDiminishedDialog(id: String) { + suspend fun showDiminishedDialog( + id: Campaign.CharacterInstance.Id, + ) { val diminished = 0 // TODO repository.characterDiminishedFlow(id = id).value val textFieldValue = mutableStateOf( TextFieldValue("$diminished", selection = TextRange(index = 0)) @@ -48,4 +51,6 @@ class CharacterDiminishedViewModel( // diminished = value, // ) } + + } \ No newline at end of file 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 29ec973..fd5cead 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 @@ -26,6 +26,7 @@ import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage 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.ic_heart_24dp import lwacharactersheet.composeapp.generated.resources.ic_water_drop_24dp @@ -33,8 +34,9 @@ import org.jetbrains.compose.resources.painterResource @Stable data class PlayerPortraitUio( - val id: String, + val id: Campaign.CharacterInstance.Id, val portrait: String?, + val name: String, val hp: Int, val maxHp: Int, val pp: Int, @@ -46,7 +48,7 @@ fun PlayerPortrait( modifier: Modifier = Modifier, size: DpSize, character: PlayerPortraitUio, - onCharacter: (id: String) -> Unit, + onCharacter: (id: Campaign.CharacterInstance.Id) -> Unit, ) { val colorScheme = MaterialTheme.lwa.colorScheme 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 cda5d3d..7a77c5f 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 @@ -1,7 +1,6 @@ package com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.lazy.LazyColumn @@ -11,6 +10,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp +import com.pixelized.shared.lwa.model.campaign.Campaign import org.koin.compose.viewmodel.koinViewModel object PlayerRibbon { @@ -24,7 +24,7 @@ fun PlayerRibbon( modifier: Modifier = Modifier, playerRibbonViewModel: PlayerRibbonViewModel = koinViewModel(), padding: PaddingValues = PaddingValues(all = 8.dp), - onCharacter: (id: String) -> Unit, + onCharacter: (id: Campaign.CharacterInstance.Id) -> Unit, ) { val characters = playerRibbonViewModel.characters.collectAsState() @@ -45,7 +45,7 @@ fun PlayerRibbon( ) PlayerPortraitRoll( size = PlayerRibbon.Default.size, - value = playerRibbonViewModel.roll(characterId = it.id).value, + value = playerRibbonViewModel.roll(characterSheetId = it.id.characterSheetId).value, onRightClick = { playerRibbonViewModel.onPortraitRollRightClick(characterId = it.characterId) }, 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 415122b..11098b7 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 @@ -1,48 +1,36 @@ package com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon -import com.pixelized.desktop.lwa.business.ExpressionUseCase -import com.pixelized.desktop.lwa.parser.expression.Expression -import com.pixelized.desktop.lwa.repository.alteration.model.FieldAlteration +import com.pixelized.shared.lwa.model.AlteredCharacterSheetFactory +import com.pixelized.shared.lwa.model.alteration.FieldAlteration import com.pixelized.shared.lwa.model.campaign.Campaign import com.pixelized.shared.lwa.model.campaign.damage import com.pixelized.shared.lwa.model.campaign.power import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet -import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.HP -import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.PP -import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.THUMBNAIL class PlayerRibbonFactory( - private val expressionUseCase: ExpressionUseCase, + private val alteredCharacterSheetFactory: AlteredCharacterSheetFactory, ) { - fun convertToPlayerPortraitUio( characterSheet: CharacterSheet?, + characterInstanceId: Campaign.CharacterInstance.Id, characterInstance: Campaign.CharacterInstance, alterations: Map>, ): PlayerPortraitUio? { if (characterSheet == null) return null - fun List?.sum(): Int { - return this?.sumOf { - expressionUseCase.computeExpression( - sheet = characterSheet, - expression = it.expression, - ) - } ?: 0 - } - - val maxHp = characterSheet.hp + alterations[HP].sum() - val maxPp = characterSheet.pp + alterations[PP].sum() + val alteredCharacterSheet = alteredCharacterSheetFactory.sheet( + characterSheet = characterSheet, + alterations = alterations, + ) return PlayerPortraitUio( - id = characterSheet.id, - portrait = alterations[THUMBNAIL] - ?.firstNotNullOfOrNull { (it.expression as? Expression.UrlExpression)?.url } - ?: characterSheet.thumbnail, - hp = maxHp - characterInstance.damage, - maxHp = maxHp, - pp = maxPp - characterInstance.power, - maxPp = maxPp, + id = characterInstanceId, + portrait = alteredCharacterSheet.thumbnail, + name = alteredCharacterSheet.name, + hp = alteredCharacterSheet.maxHp - characterInstance.damage, + maxHp = alteredCharacterSheet.maxHp, + pp = alteredCharacterSheet.maxPp - characterInstance.power, + maxPp = alteredCharacterSheet.maxPp, ) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/PlayerRibbonViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/PlayerRibbonViewModel.kt index 0a442a4..b3c2a91 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 @@ -20,6 +20,7 @@ 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( private val rollHistoryRepository: RollHistoryRepository, @@ -35,18 +36,21 @@ class PlayerRibbonViewModel( combine>( flows = campaign.characters.map { entry -> combine( - characterRepository.characterDetailFlow(characterId = entry.key), + characterRepository.characterDetailFlow(characterId = entry.key.characterSheetId), alterationRepository.alterationsFlow(characterId = entry.key), ) { sheet, alterations -> ribbonFactory.convertToPlayerPortraitUio( characterSheet = sheet, + characterInstanceId = entry.key, characterInstance = entry.value, alterations = alterations, ) } }, transform = { headers -> - headers.mapNotNull { it }.toList() + headers.mapNotNull { it } + .sortedWith(compareBy(Collator.getInstance()) { it.name }) + .toList() } ) } @@ -63,7 +67,7 @@ class PlayerRibbonViewModel( campaignRepository.campaignFlow.collectLatest { it.characters.keys.forEach { id -> characterRepository.characterDetail( - characterId = id, + characterSheetId = id.characterSheetId, forceUpdate = true, ) } @@ -73,13 +77,13 @@ class PlayerRibbonViewModel( @Composable @Stable - fun roll(characterId: String): State { - val state = rolls.getOrPut(characterId) { mutableStateOf(null) } - LaunchedEffect(characterId) { + fun roll(characterSheetId: String): State { + val state = rolls.getOrPut(characterSheetId) { mutableStateOf(null) } + LaunchedEffect(characterSheetId) { rollHistoryRepository.rolls.collect { roll -> - if (roll.characterId == characterId) { + if (roll.characterId == characterSheetId) { state.value = PlayerPortraitRollUio( - characterId = characterId, + characterId = characterSheetId, value = roll.rollValue, label = roll.resultLabel?.split(" ")?.joinToString(separator = "\n") { it } ) 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 3928b18..e2a184f 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,16 +1,17 @@ package com.pixelized.desktop.lwa.ui.screen.characterSheet.detail -import com.pixelized.desktop.lwa.business.ExpressionUseCase -import com.pixelized.desktop.lwa.repository.alteration.model.FieldAlteration import com.pixelized.desktop.lwa.ui.composable.tooltip.TooltipUio 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.shared.lwa.model.AlteredCharacterSheetFactory +import com.pixelized.shared.lwa.model.alteration.FieldAlteration import com.pixelized.shared.lwa.model.campaign.Campaign import com.pixelized.shared.lwa.model.campaign.character import com.pixelized.shared.lwa.model.campaign.damage import com.pixelized.shared.lwa.model.campaign.power import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId +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 @@ -43,34 +44,32 @@ import lwacharactersheet.composeapp.generated.resources.tooltip__sub_characteris import org.jetbrains.compose.resources.getString class CharacterSheetFactory( + private val alteredCharacterSheetFactory: AlteredCharacterSheetFactory, private val skillUseCase: ExpressionUseCase, - private val expressionUseCase: ExpressionUseCase, ) { suspend fun convertToUio( - sheet: CharacterSheet?, + characterSheet: CharacterSheet?, + instanceId: Campaign.CharacterInstance.Id, campaign: Campaign, alterations: Map>, ): CharacterSheetPageUio? { - if (sheet == null) return null + if (characterSheet == null) return null - fun List?.sum(): Int { - return this?.sumOf { - expressionUseCase.computeExpression(sheet = sheet, expression = it.expression) - } ?: 0 - } + val alteredSheet = alteredCharacterSheetFactory.sheet( + characterSheet = characterSheet, + alterations = alterations, + ) - val maxHp = sheet.hp + alterations[CharacteristicId.HP].sum() - val maxPp = sheet.pp + alterations[CharacteristicId.PP].sum() - val instance = campaign.character(sheet.id) + val instance = campaign.character(id = instanceId) return CharacterSheetPageUio( - id = sheet.id, - name = sheet.name, + id = alteredSheet.id, + name = alteredSheet.name, characteristics = listOf( Characteristic( id = CharacteristicId.STR, label = getString(Res.string.character_sheet__characteristics__str), - value = "${sheet.strength + alterations[CharacteristicId.STR].sum()}", + value = "${alteredSheet.strength}", tooltips = TooltipUio( title = getString(Res.string.character_sheet__characteristics__str), description = getString(Res.string.tooltip__characteristics__strength), @@ -80,7 +79,7 @@ class CharacterSheetFactory( Characteristic( id = CharacteristicId.DEX, label = getString(Res.string.character_sheet__characteristics__dex), - value = "${sheet.dexterity + alterations[CharacteristicId.DEX].sum()}", + value = "${alteredSheet.dexterity}", tooltips = TooltipUio( title = getString(Res.string.character_sheet__characteristics__dex), description = getString(Res.string.tooltip__characteristics__dexterity), @@ -90,7 +89,7 @@ class CharacterSheetFactory( Characteristic( id = CharacteristicId.CON, label = getString(Res.string.character_sheet__characteristics__con), - value = "${sheet.constitution + alterations[CharacteristicId.CON].sum()}", + value = "${alteredSheet.constitution}", tooltips = TooltipUio( title = getString(Res.string.character_sheet__characteristics__con), description = getString(Res.string.tooltip__characteristics__constitution), @@ -100,7 +99,7 @@ class CharacterSheetFactory( Characteristic( id = CharacteristicId.HEI, label = getString(Res.string.character_sheet__characteristics__hei), - value = "${sheet.height + alterations[CharacteristicId.HEI].sum()}", + value = "${alteredSheet.height}", tooltips = TooltipUio( title = getString(Res.string.character_sheet__characteristics__hei), description = getString(Res.string.tooltip__characteristics__height), @@ -110,7 +109,7 @@ class CharacterSheetFactory( Characteristic( id = CharacteristicId.INT, label = getString(Res.string.character_sheet__characteristics__int), - value = "${sheet.intelligence + alterations[CharacteristicId.INT].sum()}", + value = "${alteredSheet.intelligence}", tooltips = TooltipUio( title = getString(Res.string.character_sheet__characteristics__int), description = getString(Res.string.tooltip__characteristics__intelligence), @@ -120,7 +119,7 @@ class CharacterSheetFactory( Characteristic( id = CharacteristicId.POW, label = getString(Res.string.character_sheet__characteristics__pow), - value = "${sheet.power + alterations[CharacteristicId.POW].sum()}", + value = "${alteredSheet.power}", tooltips = TooltipUio( title = getString(Res.string.character_sheet__characteristics__pow), description = getString(Res.string.tooltip__characteristics__power), @@ -130,7 +129,7 @@ class CharacterSheetFactory( Characteristic( id = CharacteristicId.CHA, label = getString(Res.string.character_sheet__characteristics__cha), - value = "${sheet.charisma + alterations[CharacteristicId.CHA].sum()}", + value = "${alteredSheet.charisma}", tooltips = TooltipUio( title = getString(Res.string.character_sheet__characteristics__cha), description = getString(Res.string.tooltip__characteristics__charisma), @@ -142,7 +141,7 @@ class CharacterSheetFactory( Characteristic( id = CharacteristicId.MOV, label = getString(Res.string.character_sheet__sub_characteristics__movement), - value = "${sheet.movement + alterations[CharacteristicId.MOV].sum()}", + value = "${alteredSheet.movement}", tooltips = TooltipUio( title = getString(Res.string.character_sheet__sub_characteristics__movement), description = getString(Res.string.tooltip__sub_characteristics__movement), @@ -152,7 +151,7 @@ class CharacterSheetFactory( Characteristic( id = CharacteristicId.HP, label = getString(Res.string.character_sheet__sub_characteristics__hit_point), - value = "${maxHp - instance.damage}/${maxHp}", + value = alteredSheet.maxHp.let { maxHp -> "${maxHp - instance.damage}/${maxHp}" }, tooltips = TooltipUio( title = getString(Res.string.character_sheet__sub_characteristics__hit_point), description = getString(Res.string.tooltip__sub_characteristics__hit_point), @@ -162,7 +161,7 @@ class CharacterSheetFactory( Characteristic( id = CharacteristicId.PP, label = getString(Res.string.character_sheet__sub_characteristics__power_point), - value = "${maxPp - instance.power}/${maxPp}", + value = alteredSheet.maxPp.let { maxPp -> "${maxPp - instance.power}/${maxPp}" }, tooltips = TooltipUio( title = getString(Res.string.character_sheet__sub_characteristics__power_point), description = getString(Res.string.tooltip__sub_characteristics__power_point), @@ -172,7 +171,7 @@ class CharacterSheetFactory( Characteristic( id = CharacteristicId.DMG, label = getString(Res.string.character_sheet__sub_characteristics__damage_bonus), - value = sheet.damageBonus, + value = alteredSheet.damageBonus, tooltips = TooltipUio( title = getString(Res.string.character_sheet__sub_characteristics__damage_bonus), description = getString(Res.string.tooltip__sub_characteristics__bonus_damage), @@ -182,7 +181,7 @@ class CharacterSheetFactory( Characteristic( id = CharacteristicId.ARMOR, label = getString(Res.string.character_sheet__sub_characteristics__armor), - value = "${sheet.armor + alterations[CharacteristicId.ARMOR].sum()}", + value = "${alteredSheet.armor}", tooltips = TooltipUio( title = getString(Res.string.character_sheet__sub_characteristics__armor), description = getString(Res.string.tooltip__sub_characteristics__armor), @@ -192,7 +191,7 @@ class CharacterSheetFactory( Characteristic( id = CharacteristicId.LB, label = getString(Res.string.character_sheet__sub_characteristics__learning), - value = "${sheet.learning + alterations[CharacteristicId.LB].sum()}", + value = "${alteredSheet.learning}", tooltips = TooltipUio( title = getString(Res.string.character_sheet__sub_characteristics__learning), description = getString(Res.string.tooltip__sub_characteristics__learning), @@ -202,7 +201,7 @@ class CharacterSheetFactory( Characteristic( id = CharacteristicId.GHP, label = getString(Res.string.character_sheet__sub_characteristics__hp_grow), - value = "${sheet.hpGrow + alterations[CharacteristicId.GHP].sum()}", + value = "${alteredSheet.hpGrow}", tooltips = TooltipUio( title = getString(Res.string.character_sheet__sub_characteristics__hp_grow), description = getString(Res.string.tooltip__sub_characteristics__hp_grow), @@ -210,14 +209,14 @@ class CharacterSheetFactory( editable = false, ), ), - commonSkills = sheet.commonSkills.map { skill -> + commonSkills = characterSheet.commonSkills.map { skill -> Node( id = skill.id, label = skill.label, value = skillUseCase.computeSkillValue( - sheet = sheet, + sheet = characterSheet, skill = skill, - alterations = alterations[skill.id].sum(), + alterations = alterations, ), tooltips = skill.description?.let { TooltipUio( @@ -228,7 +227,7 @@ class CharacterSheetFactory( used = skill.used, ) }, - specialSKills = sheet.specialSkills.map { skill -> + specialSKills = characterSheet.specialSkills.map { skill -> Node( id = skill.id, label = skill.label, @@ -239,14 +238,14 @@ class CharacterSheetFactory( ) }, value = skillUseCase.computeSkillValue( - sheet = sheet, + sheet = characterSheet, skill = skill, - alterations = alterations[skill.id].sum(), + alterations = alterations, ), used = skill.used, ) }, - magicsSkills = sheet.magicSkills.map { skill -> + magicsSkills = characterSheet.magicSkills.map { skill -> Node( id = skill.id, label = skill.label, @@ -257,14 +256,14 @@ class CharacterSheetFactory( ) }, value = skillUseCase.computeSkillValue( - sheet = sheet, + sheet = characterSheet, skill = skill, - alterations = alterations[skill.id].sum(), + alterations = alterations, ), used = skill.used, ) }, - actions = sheet.actions.mapNotNull { + actions = characterSheet.actions.mapNotNull { if (it.roll.isNotEmpty()) { CharacterSheetPageUio.Roll( label = it.label, 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 e79f6fa..dbe2449 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 @@ -65,7 +65,6 @@ import com.pixelized.desktop.lwa.ui.navigation.window.LocalWindow import com.pixelized.desktop.lwa.ui.navigation.window.destination.navigateToCharacterSheetEdit import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.CharacterSheetPageUio.Characteristic import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.dialog.CharacterSheetDeleteConfirmationDialog -import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.dialog.CharacterSheetStatDialog 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.RollPage @@ -147,11 +146,10 @@ fun CharacterSheetPage( characterSheet = sheet, diminishedValue = viewModel.diminishedValue, onDiminished = { -// blurController.show() -// scope.launch { -// viewModel.showDiminishedDialog() -// } - viewModel.toggleWolf() + blurController.show() + scope.launch { + viewModel.showDiminishedDialog() + } }, onEdit = { windowController.navigateToCharacterSheetEdit( @@ -172,10 +170,10 @@ fun CharacterSheetPage( viewModel.showRollOverlay() }, onSubCharacteristic = { - blurController.show() - scope.launch { - viewModel.showSubCharacteristicDialog(id = it.id) - } +// blurController.show() +// scope.launch { +// viewModel.showSubCharacteristicDialog(id = it.id) +// } }, onSkill = { node -> blurController.show() @@ -232,22 +230,6 @@ fun CharacterSheetPage( }, ) - CharacterSheetStatDialog( - dialog = viewModel.statChangeDialog, - onConfirm = { - viewModel.changeSubCharacteristic( - characteristicId = it.id, - value = it.value().text.toIntOrNull() ?: 0, - ) - viewModel.hideSubCharacteristicDialog() - blurController.hide() - }, - onDismissRequest = { - viewModel.hideSubCharacteristicDialog() - blurController.hide() - } - ) - DiminishedStatDialog( dialog = viewModel.diminishedDialog, onConfirm = { 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 9e2b0cf..cd47b3d 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 @@ -5,8 +5,6 @@ import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.ui.text.TextRange -import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -17,12 +15,6 @@ import com.pixelized.desktop.lwa.repository.network.NetworkRepository import com.pixelized.desktop.lwa.ui.navigation.screen.destination.CharacterSheetDestination import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.dialog.CharacterSheetDeleteConfirmationDialogUio import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.dialog.DiminishedStatDialogUio -import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.dialog.StatChangeDialogUio -import com.pixelized.shared.lwa.model.campaign.Campaign.CharacterInstance.Characteristic -import com.pixelized.shared.lwa.model.campaign.damage -import com.pixelized.shared.lwa.model.campaign.power -import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet -import com.pixelized.shared.lwa.protocol.websocket.payload.UpdatePlayerCharacteristicMessage import com.pixelized.shared.lwa.protocol.websocket.payload.UpdateSkillUsageMessage import kotlinx.coroutines.flow.SharingStarted @@ -30,10 +22,6 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.serialization.json.Json -import lwacharactersheet.composeapp.generated.resources.Res -import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__sub_characteristics__hit_point -import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__sub_characteristics__power_point -import org.jetbrains.compose.resources.getString private typealias CSDCDialogUio = CharacterSheetDeleteConfirmationDialogUio @@ -55,9 +43,6 @@ class CharacterSheetViewModel( private val _displayRollOverlay = mutableStateOf(false) val displayRollOverlay: State get() = _displayRollOverlay - private val _statChangeDialog = mutableStateOf(null) - val statChangeDialog: State get() = _statChangeDialog - private val _diminishedDialog = mutableStateOf(null) val diminishedDialog: State get() = _diminishedDialog @@ -71,12 +56,13 @@ class CharacterSheetViewModel( get() = remember { mutableStateOf(null) } private val sheetFlow = combine( - characterRepository.characterDetailFlow(characterId = argument.id), + characterRepository.characterDetailFlow(characterId = argument.characterInstanceId.characterSheetId), campaignRepository.campaignFlow, - alteration.alterationsFlow(characterId = argument.id), + alteration.alterationsFlow(characterId = argument.characterInstanceId), transform = { sheet, campaign, alterations -> factory.convertToUio( - sheet = sheet, + characterSheet = sheet, + instanceId = argument.characterInstanceId, campaign = campaign, alterations = alterations ) @@ -90,10 +76,6 @@ class CharacterSheetViewModel( @Composable get() = sheetFlow.collectAsState() - fun toggleWolf() { - alteration.toggle(argument.id, "65e37d32-3031-4bf8-9369-d2c45d2efac0") - } - suspend fun deleteCharacter(id: String) { characterRepository.deleteCharacter(characterId = id) } @@ -102,7 +84,7 @@ class CharacterSheetViewModel( viewModelScope.launch { network.share( payload = UpdateSkillUsageMessage( - characterId = argument.id, + characterId = argument.characterInstanceId.characterSheetId, skillId = skill.id, ) ) @@ -111,7 +93,7 @@ class CharacterSheetViewModel( fun showConfirmCharacterDeletionDialog() { characterRepository.characterPreview( - characterId = argument.id + characterId = argument.characterInstanceId.characterSheetId )?.let { preview -> _displayDeleteConfirmationDialog.value = CharacterSheetDeleteConfirmationDialogUio( id = preview.id, @@ -124,74 +106,6 @@ class CharacterSheetViewModel( _displayDeleteConfirmationDialog.value = null } - suspend fun showSubCharacteristicDialog(id: String) { - characterRepository.characterDetail( - characterId = argument.id, - )?.let { sheet -> - val instance = campaignRepository.characterInstanceFlow(id = argument.id).value - _statChangeDialog.value = when (id) { - CharacterSheet.CharacteristicId.HP -> { - val value = mutableStateOf( - "${sheet.hp - instance.damage}".let { - TextFieldValue(text = it, selection = TextRange(it.length)) - } - ) - StatChangeDialogUio( - id = Characteristic.Damage, - label = getString(resource = Res.string.character_sheet_edit__sub_characteristics__hit_point), - value = { value.value }, - onValueChange = { value.value = it }, - maxValue = "${sheet.hp}", - ) - } - - CharacterSheet.CharacteristicId.PP -> { - val value = mutableStateOf( - "${sheet.power - instance.power}".let { - TextFieldValue(text = it, selection = TextRange(it.length)) - } - ) - StatChangeDialogUio( - id = Characteristic.Power, - label = getString(resource = Res.string.character_sheet_edit__sub_characteristics__power_point), - value = { value.value }, - onValueChange = { value.value = it }, - maxValue = "${sheet.power}", - ) - } - - else -> null - } - } - } - - fun hideSubCharacteristicDialog() { - _statChangeDialog.value = null - } - - fun changeSubCharacteristic( - characteristicId: Characteristic, - value: Int, - ) { - viewModelScope.launch { - characterRepository.characterDetail( - characterId = argument.id, - )?.let { sheet -> - network.share( - payload = UpdatePlayerCharacteristicMessage( - characterId = argument.id, - characteristic = characteristicId, - value = when (characteristicId) { - Characteristic.Damage -> sheet.hp - value - Characteristic.Power -> sheet.pp - value - else -> sheet.movement - value - }, - ), - ) - } - } - } - fun showRollOverlay() { _displayRollOverlay.value = true } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/characterSheet/detail/dialog/DiminishedStatDialog.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/characterSheet/detail/dialog/DiminishedStatDialog.kt index af0773e..cee44be 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/characterSheet/detail/dialog/DiminishedStatDialog.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/characterSheet/detail/dialog/DiminishedStatDialog.kt @@ -42,6 +42,7 @@ import androidx.compose.ui.text.input.TextFieldValue 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.shared.lwa.model.campaign.Campaign import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.dialog__cancel_action import lwacharactersheet.composeapp.generated.resources.dialog__confirm_action @@ -49,7 +50,7 @@ import org.jetbrains.compose.resources.stringResource @Stable data class DiminishedStatDialogUio( - val id: String, + val id: Campaign.CharacterInstance.Id, val label: String, val value: () -> TextFieldValue, val onValueChange: (TextFieldValue) -> Unit, 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 db69c7c..ab45c60 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 @@ -90,20 +90,8 @@ class CharacterSheetEditFactory( intelligence = intelligence, power = power, charisma = charisma, - hp = characterSheetUseCase.defaultMaxHp( - constitution = constitution, - height = height, - level = level - ), - pp = characterSheetUseCase.defaultMaxPower(power = power), - movement = characterSheetUseCase.defaultMovement(), - damageBonus = characterSheetUseCase.defaultDamageBonus( - strength = strength, - height = height - ), - armor = characterSheetUseCase.defaultArmor(), - learning = characterSheetUseCase.defaultLearning(intelligence = intelligence), - hpGrow = characterSheetUseCase.defaultHpGrow(constitution = constitution), + movement = characterSheetUseCase.movement(), + armor = characterSheetUseCase.armor(), commonSkills = editedSheet.commonSkills.map { editedSkill -> val currentSkill = currentSheet?.commonSkills?.firstOrNull { it.id == editedSkill.id diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/characterSheet/edit/CharacterSheetEditViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/characterSheet/edit/CharacterSheetEditViewModel.kt index 08be8c2..9be7f37 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/characterSheet/edit/CharacterSheetEditViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/characterSheet/edit/CharacterSheetEditViewModel.kt @@ -29,7 +29,7 @@ class CharacterSheetEditViewModel( private val _characterSheet = mutableStateOf( runBlocking { sheetFactory.convertToUio( - sheet = characterSheetRepository.characterDetail(characterId = argument.id), + sheet = characterSheetRepository.characterDetail(characterSheetId = argument.id), onDeleteSkill = ::deleteSkill, ) } @@ -108,7 +108,7 @@ class CharacterSheetEditViewModel( suspend fun save() { val updatedSheet = sheetFactory.updateCharacterSheet( - currentSheet = characterSheetRepository.characterDetail(characterId = _characterSheet.value.id), + currentSheet = characterSheetRepository.characterDetail(characterSheetId = _characterSheet.value.id), editedSheet = _characterSheet.value, ) characterSheetRepository.updateCharacter( diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/main/MainPage.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/main/MainPage.kt index dcfe8d9..e87f43b 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/main/MainPage.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/main/MainPage.kt @@ -18,6 +18,8 @@ import androidx.compose.material.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.State +import androidx.compose.runtime.collectAsState + import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign @@ -29,6 +31,8 @@ import com.pixelized.desktop.lwa.ui.navigation.screen.destination.navigateToNetw import com.pixelized.desktop.lwa.ui.navigation.window.destination.navigateToCharacterSheet import com.pixelized.desktop.lwa.ui.navigation.window.destination.navigateToCharacterSheetEdit import com.pixelized.desktop.lwa.ui.navigation.window.destination.navigateToRollHistory + +import com.pixelized.shared.lwa.model.campaign.Campaign import kotlinx.coroutines.runBlocking import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__create__title @@ -47,7 +51,7 @@ import org.koin.compose.viewmodel.koinViewModel @Stable data class CharacterUio( - val id: String, + val id: Campaign.CharacterInstance.Id, val name: String, ) @@ -57,6 +61,7 @@ fun MainPage( ) { val window = LocalWindowController.current val screen = LocalScreenController.current + val characters = viewModel.characters.collectAsState() Surface( modifier = Modifier.fillMaxSize(), @@ -69,7 +74,7 @@ fun MainPage( contentAlignment = Alignment.Center, ) { MainPageContent( - characters = viewModel.characters, + characters = characters, enableRollHistory = viewModel.enableRollHistory, onCharacter = { window.navigateToCharacterSheet( 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 358b8ff..345cb63 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 @@ -3,30 +3,52 @@ package com.pixelized.desktop.lwa.ui.screen.main import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.lordcodes.turtle.shellRun +import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository import com.pixelized.desktop.lwa.repository.network.NetworkRepository import com.pixelized.desktop.lwa.utils.extention.collectAsState import com.pixelized.shared.lwa.OperatingSystem import com.pixelized.shared.lwa.storePath +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.stateIn class MainPageViewModel( - private val repository: CharacterSheetRepository, + private val characterSheetRepository: CharacterSheetRepository, + private val campaignRepository: CampaignRepository, networkRepository: NetworkRepository, ) : ViewModel() { - val characters: State> - @Composable - get() = repository - .characterSheetPreviewFlow - .collectAsState { sheets -> - sheets.map { sheet -> - CharacterUio( - id = sheet.id, - name = sheet.name, - ) + @OptIn(ExperimentalCoroutinesApi::class) + val characters: StateFlow> = campaignRepository.campaignFlow + .flatMapLatest { campaign -> + combine( + campaign.characters.map { entry -> + characterSheetRepository.characterDetailFlow(characterId = entry.key.characterSheetId) + .mapNotNull { sheet -> + sheet?.let { + CharacterUio( + id = entry.key, + name = it.name, + ) + } + } } + ) { + it.asList() } + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = emptyList(), + ) private val networkStatus = networkRepository.status val enableRollHistory: State 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 index 864140e..6104ba5 100644 --- 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 @@ -6,8 +6,8 @@ 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.business.ExpressionUseCase -import com.pixelized.desktop.lwa.business.SkillStepUseCase +import com.pixelized.shared.lwa.usecase.ExpressionUseCase +import com.pixelized.shared.lwa.usecase.SkillStepUseCase import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository import com.pixelized.desktop.lwa.repository.roll_history.RollHistoryRepository import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.CharacterSheetPageUio @@ -101,7 +101,7 @@ class RollViewModel( this.sheet = runBlocking { rollRotation.snapTo(0f) rollScale.snapTo(1f) - characterSheetRepository.characterDetail(characterId = sheet.id)!! + characterSheetRepository.characterDetail(characterSheetId = sheet.id)!! } this.rollAction = rollAction @@ -172,6 +172,7 @@ class RollViewModel( val roll = skillComputation.computeRoll( sheet = sheet, + alterations = emptyMap(), // TODO ? expression = rollAction, ) diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/business/SettingsUseCase.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/usecase/SettingsUseCase.kt similarity index 89% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/business/SettingsUseCase.kt rename to composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/usecase/SettingsUseCase.kt index 219a68e..2a71f22 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/business/SettingsUseCase.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/usecase/SettingsUseCase.kt @@ -1,4 +1,4 @@ -package com.pixelized.desktop.lwa.business +package com.pixelized.desktop.lwa.usecase import com.pixelized.desktop.lwa.repository.settings.model.Settings 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 2722e7c..f2f8fb9 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.defaultDamageBonus(sum = it) + val result = userCase.damageBonus(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.defaultDamageBonus(sum = it) + val result = userCase.damageBonus(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.defaultDamageBonus(sum = it) + val result = userCase.damageBonus(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.defaultDamageBonus(sum = it) + val result = userCase.damageBonus(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.defaultDamageBonus(sum = it) + val result = userCase.damageBonus(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.defaultDamageBonus(sum = it) + val result = userCase.damageBonus(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/business/RollUseCaseTest.kt b/composeApp/src/commonTest/kotlin/com/pixelized/desktop/lwa/business/RollUseCaseTest.kt index 9a0aac1..4e5bd50 100644 --- a/composeApp/src/commonTest/kotlin/com/pixelized/desktop/lwa/business/RollUseCaseTest.kt +++ b/composeApp/src/commonTest/kotlin/com/pixelized/desktop/lwa/business/RollUseCaseTest.kt @@ -1,5 +1,6 @@ package com.pixelized.desktop.lwa.business +import com.pixelized.shared.lwa.usecase.RollUseCase import org.junit.Test class RollUseCaseTest { diff --git a/composeApp/src/commonTest/kotlin/com/pixelized/desktop/lwa/business/SkillStepUseCaseTest.kt b/composeApp/src/commonTest/kotlin/com/pixelized/desktop/lwa/business/SkillStepUseCaseTest.kt index 6cdbaef..24d7bed 100644 --- a/composeApp/src/commonTest/kotlin/com/pixelized/desktop/lwa/business/SkillStepUseCaseTest.kt +++ b/composeApp/src/commonTest/kotlin/com/pixelized/desktop/lwa/business/SkillStepUseCaseTest.kt @@ -1,6 +1,7 @@ package com.pixelized.desktop.lwa.business -import com.pixelized.desktop.lwa.business.SkillStepUseCase.SkillStep +import com.pixelized.shared.lwa.usecase.SkillStepUseCase +import com.pixelized.shared.lwa.usecase.SkillStepUseCase.SkillStep import org.junit.Test class SkillStepUseCaseTest { diff --git a/composeApp/src/commonTest/kotlin/com/pixelized/desktop/lwa/parser/dice/DiceParserTest.kt b/composeApp/src/commonTest/kotlin/com/pixelized/desktop/lwa/parser/dice/DiceParserTest.kt index f7c5622..b24c724 100644 --- a/composeApp/src/commonTest/kotlin/com/pixelized/desktop/lwa/parser/dice/DiceParserTest.kt +++ b/composeApp/src/commonTest/kotlin/com/pixelized/desktop/lwa/parser/dice/DiceParserTest.kt @@ -1,5 +1,7 @@ package com.pixelized.desktop.lwa.parser.dice +import com.pixelized.shared.lwa.parser.dice.Dice +import com.pixelized.shared.lwa.parser.dice.DiceParser import org.junit.Test class DiceParserTest { 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 eed0fe1..ca059e3 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 @@ -1,8 +1,10 @@ package com.pixelized.desktop.lwa.parser.expression -import com.pixelized.desktop.lwa.parser.dice.DiceParser -import com.pixelized.desktop.lwa.parser.expression.ExpressionParser.Error -import com.pixelized.desktop.lwa.parser.word.WordParser +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.WordParser import org.junit.Test import kotlin.test.assertFailsWith @@ -120,6 +122,20 @@ class ExpressionParserTest { ) } + @Test + fun testReadWrite() { + val parser = ExpressionParser( + diceParser = DiceParser(), + wordParser = WordParser(), + ) + parser.test( + expression = "((1+2)*3)", + ) + parser.test( + expression = "(1+(2*3))", + ) + } + private fun ExpressionParser.test( expression: String, expected: Expression?, @@ -129,4 +145,13 @@ class ExpressionParserTest { "ExpressionParser.parse(input=$expression) is expected to return:$expected, but was:$result" } } + + private fun ExpressionParser.test( + expression: String, + ) { + val result = parse(parse(expression)?.toString())?.toString() + assert(result == expression) { + "ExpressionParser.parse(input=$expression) is expected to return:$expression, but was:$result" + } + } } \ No newline at end of file diff --git a/composeApp/src/commonTest/kotlin/com/pixelized/desktop/lwa/parser/word/WordParserTest.kt b/composeApp/src/commonTest/kotlin/com/pixelized/desktop/lwa/parser/word/WordParserTest.kt index baeb9ed..67e085b 100644 --- a/composeApp/src/commonTest/kotlin/com/pixelized/desktop/lwa/parser/word/WordParserTest.kt +++ b/composeApp/src/commonTest/kotlin/com/pixelized/desktop/lwa/parser/word/WordParserTest.kt @@ -1,5 +1,7 @@ package com.pixelized.desktop.lwa.parser.word +import com.pixelized.shared.lwa.parser.word.Word +import com.pixelized.shared.lwa.parser.word.WordParser import org.junit.Test class WordParserTest { diff --git a/server/src/main/kotlin/Module.kt b/server/src/main/kotlin/Module.kt index 4fba9bc..caf1787 100644 --- a/server/src/main/kotlin/Module.kt +++ b/server/src/main/kotlin/Module.kt @@ -1,3 +1,5 @@ +import com.pixelized.server.lwa.model.alteration.AlterationService +import com.pixelized.server.lwa.model.alteration.AlterationStore import com.pixelized.server.lwa.model.campaign.CampaignService import com.pixelized.server.lwa.model.campaign.CampaignStore import com.pixelized.server.lwa.model.character.CharacterSheetService @@ -26,12 +28,14 @@ val storeDependencies get() = module { singleOf(::CharacterSheetStore) singleOf(::CampaignStore) + singleOf(::AlterationStore) } val serviceDependencies get() = module { singleOf(::CharacterSheetService) singleOf(::CampaignService) + singleOf(::AlterationService) } val factoryDependencies diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/alteration/AlterationService.kt b/server/src/main/kotlin/com/pixelized/server/lwa/model/alteration/AlterationService.kt new file mode 100644 index 0000000..a411720 --- /dev/null +++ b/server/src/main/kotlin/com/pixelized/server/lwa/model/alteration/AlterationService.kt @@ -0,0 +1,56 @@ +package com.pixelized.server.lwa.model.alteration + +import com.pixelized.shared.lwa.model.alteration.AlterationJson +import com.pixelized.shared.lwa.model.campaign.Campaign +import com.pixelized.shared.lwa.model.campaign.CampaignJsonFactory +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + +class AlterationService( + private val store: AlterationStore, + private val campaignJsonFactory: CampaignJsonFactory, +) { + private val scope = CoroutineScope(Dispatchers.IO + Job()) + private val alterations = store.alterationsFlow() + private val actives = store.activeFlow() + .map { data -> + data.mapKeys { it: Map.Entry> -> + campaignJsonFactory.convertFromV1(characterInstanceIdJson = it.key) + } + }.stateIn( + scope = scope, + started = SharingStarted.Eagerly, + initialValue = emptyMap() + ) + + fun alterations(): List { + return alterations.value + } + + fun active( + characterInstanceId: Campaign.CharacterInstance.Id, + ): List { + return actives.value[characterInstanceId] ?: emptyList() + } + + fun isAlterationActive( + characterInstanceId: Campaign.CharacterInstance.Id, + alterationId: String, + ): Boolean { + return actives.value[characterInstanceId]?.contains(alterationId) ?: false + } + + suspend fun toggleActiveAlteration( + characterInstanceId: Campaign.CharacterInstance.Id, + alterationId: String, + ): Boolean { + return store.toggleActiveAlteration( + characterInstanceId = characterInstanceId, + alterationId = alterationId, + ) + } +} diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/alteration/AlterationStore.kt b/server/src/main/kotlin/com/pixelized/server/lwa/model/alteration/AlterationStore.kt new file mode 100644 index 0000000..4caa8b2 --- /dev/null +++ b/server/src/main/kotlin/com/pixelized/server/lwa/model/alteration/AlterationStore.kt @@ -0,0 +1,144 @@ +package com.pixelized.server.lwa.model.alteration + +import com.pixelized.shared.lwa.alterationsPath +import com.pixelized.shared.lwa.model.alteration.AlterationJson +import com.pixelized.shared.lwa.model.campaign.Campaign +import com.pixelized.shared.lwa.model.campaign.CampaignJsonFactory +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import java.io.File + +class AlterationStore( + private val campaignJsonFactory: CampaignJsonFactory, + private val json: Json, +) { + private val directory = File(alterationsPath()).also { it.mkdirs() } + private val alterationsFlow = MutableStateFlow>(emptyList()) + private val activeFlow = MutableStateFlow>>(emptyMap()) + + init { + // build a coroutine scope for async calls + val scope = CoroutineScope(Dispatchers.IO + Job()) + // load the initial data + scope.launch { + updateAlterations() + updateActiveAlterations() + } + } + + fun alterationsFlow(): StateFlow> = alterationsFlow + + fun activeFlow(): StateFlow>> = activeFlow + + private fun updateAlterations() { + alterationsFlow.value = loadAlterations() + } + + private fun updateActiveAlterations() { + activeFlow.value = loadActiveAlterations() + } + + private fun loadAlterations(): List { + return try { + val alterationFile = file() + val json = alterationFile.readText(charset = Charsets.UTF_8) + if (json.isBlank()) error("alterations file is empty") + this.json.decodeFromString>(json) + } catch (exception: Exception) { + // TODO log exception + emptyList() + } + } + + private fun loadActiveAlterations(): Map> { + val mainFile = file() + val jsonExt = ".json" + return directory + .listFiles() + ?.filter { file -> + // guard ignore the main alteration file and non json files. + file.name != mainFile.name && file.name.contains(jsonExt) + } + ?.mapNotNull { file -> + // read the alteration file. + val json = try { + file.readText(charset = Charsets.UTF_8) + } catch (exception: Exception) { + throw FileReadException(root = exception) + } + try { + val alterationIds = this.json.decodeFromString>(json) + val characterInstanceId = file.name.dropLast(n = jsonExt.length) + characterInstanceId to alterationIds + } catch (exception: Exception) { + // TODO log exception + throw JsonConversionException(root = exception) + } + } + ?.toMap() + ?: emptyMap() + } + + fun toggleActiveAlteration( + characterInstanceId: Campaign.CharacterInstance.Id, + alterationId: String, + ): Boolean { + val id = campaignJsonFactory.convertToJson(id = characterInstanceId) + // toggle the activation state + val characterActiveAlterationIds = activeFlow.value[id] + ?.toMutableList() + ?.toggle(alterationId = alterationId) + ?: listOf(alterationId) + // build the json string to save + val json = try { + this.json.encodeToString(characterActiveAlterationIds) + } catch (exception: Exception) { + throw JsonConversionException(root = exception) + } + // write the file + try { + val file = file(id = id) + file.writeText( + text = json, + charset = Charsets.UTF_8, + ) + } catch (exception: Exception) { + throw FileWriteException(root = exception) + } + // Update the dataflow. + activeFlow.value = activeFlow.value.toMutableMap().also { + it[id] = characterActiveAlterationIds + } + return true + } + + private fun file(): File { + return File("${alterationsPath()}alterations.json") + } + + private fun file( + id: String, + ): File { + return File("${alterationsPath()}$id.json") + } + + private fun MutableList.toggle(alterationId: String): MutableList { + if (contains(alterationId)) { + remove(alterationId) + } else { + add(alterationId) + } + return this + } + + sealed class AlterationStoreException(root: Exception) : Exception(root) + class JsonConversionException(root: Exception) : AlterationStoreException(root) + class FileWriteException(root: Exception) : AlterationStoreException(root) + class FileReadException(root: Exception) : AlterationStoreException(root) +} 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 7a776fd..1475c85 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 @@ -4,6 +4,7 @@ import com.pixelized.shared.lwa.model.campaign.Campaign import com.pixelized.shared.lwa.model.campaign.CampaignJson import com.pixelized.shared.lwa.model.campaign.CampaignJsonFactory import com.pixelized.shared.lwa.model.campaign.character +import com.pixelized.shared.lwa.model.campaign.npc import com.pixelized.shared.lwa.usecase.CampaignUseCase import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -23,11 +24,6 @@ class CampaignService( private val campaign: Campaign get() = campaignFlow.value private val campaignFlow = store.campaignFlow() - .stateIn( - scope = scope, - started = SharingStarted.Eagerly, - initialValue = Campaign.EMPTY, - ) private val campaignJsonFlow: StateFlow = campaignFlow .map { factory.convertToJson(it) } @@ -41,13 +37,15 @@ class CampaignService( return campaignJsonFlow.value } - suspend fun addCharacter(characterId: String): Boolean { + suspend fun addCharacter( + characterInstanceId: Campaign.CharacterInstance.Id, + ): Boolean { // fetch all the current campaign character val characters = campaign.characters.toMutableMap() // check if the character is in the campaign. - if (characters.containsKey(characterId)) return false + if (characters.containsKey(characterInstanceId)) return false // update the corresponding character - characters[characterId] = campaign.character(id = characterId) + characters[characterInstanceId] = campaign.character(id = characterInstanceId) // save the campaign to the disk + update the flow. store.save( campaign = campaign.copy(characters = characters) @@ -55,13 +53,15 @@ class CampaignService( return true } - suspend fun removeCharacter(characterId: String): Boolean { + suspend fun removeCharacter( + characterInstanceId: Campaign.CharacterInstance.Id, + ): Boolean { // fetch all the current campaign character val characters = campaign.characters.toMutableMap() // check if the character is in the campaign. - if (characters.containsKey(characterId).not()) return false + if (characters.containsKey(characterInstanceId).not()) return false // update the corresponding character - characters.remove(characterId) + characters.remove(characterInstanceId) // save the campaign to the disk + update the flow. store.save( campaign = campaign.copy(characters = characters) @@ -69,46 +69,50 @@ class CampaignService( return true } - suspend fun addNpc(npcId: String): Boolean { + suspend fun addNpc( + npcInstanceId: Campaign.CharacterInstance.Id, + ): Boolean { // fetch all the current campaign character - val characters = campaign.npcs.toMutableMap() + val npcs = campaign.npcs.toMutableMap() // check if the character is in the campaign. - if (characters.containsKey(npcId)) return false + if (npcs.containsKey(npcInstanceId)) return false // update the corresponding character - characters[npcId] = campaign.character(id = npcId) + npcs[npcInstanceId] = campaign.npc(id = npcInstanceId) // save the campaign to the disk + update the flow. store.save( - campaign = campaign.copy(npcs = characters) + campaign = campaign.copy(npcs = npcs) ) return true } - suspend fun removeNpc(npcId: String): Boolean { + suspend fun removeNpc( + npcInstanceId: Campaign.CharacterInstance.Id, + ): Boolean { // fetch all the current campaign character - val characters = campaign.npcs.toMutableMap() + val npcs = campaign.npcs.toMutableMap() // check if the character is in the campaign. - if (characters.containsKey(npcId).not()) return false + if (npcs.containsKey(npcInstanceId).not()) return false // update the corresponding character - characters.remove(npcId) + npcs.remove(npcInstanceId) // save the campaign to the disk + update the flow. store.save( - campaign = campaign.copy(npcs = characters) + campaign = campaign.copy(npcs = npcs) ) return true } // Data manipulation through WebSocket. - suspend fun updateCharacteristic( - characterId: String, + suspend fun updateCharacterCharacteristic( + characterInstanceId: Campaign.CharacterInstance.Id, characteristic: Campaign.CharacterInstance.Characteristic, value: Int, ) { // fetch all the current campaign character val characters = campaign.characters.toMutableMap() // update the corresponding character using the use case. - characters[characterId] = useCase.updateCharacteristic( - character = campaign.character(id = characterId), + characters[characterInstanceId] = useCase.updateCharacteristic( + character = campaign.character(id = characterInstanceId), characteristic = characteristic, value = value, ) @@ -117,4 +121,23 @@ class CampaignService( campaign = campaign.copy(characters = characters) ) } + + suspend fun updateNpcCharacteristic( + npcInstanceId: Campaign.CharacterInstance.Id, + characteristic: Campaign.CharacterInstance.Characteristic, + value: Int, + ) { + // fetch all the current campaign character + val npcs = campaign.npcs.toMutableMap() + // update the corresponding character using the use case. + npcs[npcInstanceId] = useCase.updateCharacteristic( + character = campaign.npc(id = npcInstanceId), + characteristic = characteristic, + value = value, + ) + // save the campaign to the disk + update the flow. + store.save( + campaign = campaign.copy(npcs = npcs) + ) + } } \ No newline at end of file diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/campaign/CampaignStore.kt b/server/src/main/kotlin/com/pixelized/server/lwa/model/campaign/CampaignStore.kt index 0470adf..cf41cd0 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/campaign/CampaignStore.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/model/campaign/CampaignStore.kt @@ -18,22 +18,30 @@ class CampaignStore( private val factory: CampaignJsonFactory, private val json: Json, ) { - private val directory = File(campaignPath()).also { it.mkdirs() } + private val flow = MutableStateFlow(value = Campaign.EMPTY) init { + // create the directory if needed. + File(campaignPath()).also { it.mkdirs() } + // build a coroutine scope for async calls val scope = CoroutineScope(Dispatchers.IO + Job()) + // load the initial data scope.launch { - flow.value = load() + update() } } fun campaignFlow(): StateFlow = flow + suspend fun update() { + flow.value = load() + } + suspend fun load(): Campaign { return try { val json = file().readText(charset = Charsets.UTF_8) - if (json.isBlank()) Campaign.EMPTY + if (json.isBlank()) error("Campaign file is empty") val campaign = this.json.decodeFromString(json) factory.convertFromJson(campaign) } catch (exception: Exception) { diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/Engine.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/Engine.kt index 6f3432e..f3accff 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/server/Engine.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/Engine.kt @@ -1,7 +1,9 @@ package com.pixelized.server.lwa.server +import com.pixelized.server.lwa.model.alteration.AlterationService import com.pixelized.server.lwa.model.campaign.CampaignService import com.pixelized.server.lwa.model.character.CharacterSheetService +import com.pixelized.shared.lwa.model.campaign.CampaignJsonFactory import com.pixelized.shared.lwa.protocol.websocket.Message import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation import com.pixelized.shared.lwa.protocol.websocket.payload.RollMessage @@ -12,6 +14,8 @@ import kotlinx.coroutines.flow.MutableSharedFlow class Engine( val characterService: CharacterSheetService, val campaignService: CampaignService, + val alterationService: AlterationService, + val campaignJsonFactory: CampaignJsonFactory, ) { val webSocket = MutableSharedFlow() @@ -20,11 +24,13 @@ class Engine( is RollMessage -> Unit // Nothing to do here. - is UpdatePlayerCharacteristicMessage -> campaignService.updateCharacteristic( - characterId = data.characterId, - characteristic = data.characteristic, - value = data.value, - ) + is UpdatePlayerCharacteristicMessage -> { + campaignService.updateCharacterCharacteristic( + characterInstanceId = campaignJsonFactory.convertFromV1(characterInstanceIdJson = data.characterInstanceId), + characteristic = campaignJsonFactory.convertFromV1(characteristicJson = data.characteristic), + value = data.value, + ) + } is UpdateSkillUsageMessage -> characterService.updateCharacterSkillUsage( characterId = data.characterId, @@ -36,6 +42,8 @@ class Engine( is RestSynchronisation.CharacterUpdate -> Unit // Handle in the Rest is RestSynchronisation.CharacterDelete -> Unit // Handle in the Rest + + is RestSynchronisation.ToggleActiveAlteration -> Unit // Handle in the Rest } } } \ 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 d15cea8..fa34bd9 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 @@ -3,6 +3,9 @@ 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 import com.pixelized.server.lwa.server.rest.campaign.deleteCampaignCharacter import com.pixelized.server.lwa.server.rest.campaign.deleteCampaignNpc import com.pixelized.server.lwa.server.rest.campaign.getCampaign @@ -143,6 +146,20 @@ class LocalServer { ) } } + route(path = "/alterations") { + get( + path = "", + body = engine.getAlteration(), + ) + get( + path = "/active", + body = engine.getActiveAlteration(), + ) + put( + path = "/active/toggle", + body = engine.putActiveAlteration(), + ) + } } } ) diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/alteration/GET_Active_Alteration.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/alteration/GET_Active_Alteration.kt new file mode 100644 index 0000000..e8203db --- /dev/null +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/alteration/GET_Active_Alteration.kt @@ -0,0 +1,26 @@ +package com.pixelized.server.lwa.server.rest.alteration + +import com.pixelized.server.lwa.server.Engine +import com.pixelized.shared.lwa.model.campaign.Campaign +import io.ktor.server.response.respond + +fun Engine.getActiveAlteration(): suspend io.ktor.server.routing.RoutingContext.() -> Unit { + return { + // get the query parameter + val characterSheetId = call.queryParameters["characterSheetId"] + val instanceId = call.queryParameters["instanceId"]?.toIntOrNull() + // build the character instance id. + val id = if (characterSheetId != null && instanceId != null) { + Campaign.CharacterInstance.Id( + characterSheetId = characterSheetId, + instanceId = instanceId + ) + } else { + null + } + // fetch the data from the service + val data = id?.let { alterationService.active(it) } ?: emptyList() + // respond to the client. + call.respond(data) + } +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/alteration/GET_Alteration.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/alteration/GET_Alteration.kt new file mode 100644 index 0000000..43d298a --- /dev/null +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/alteration/GET_Alteration.kt @@ -0,0 +1,10 @@ +package com.pixelized.server.lwa.server.rest.alteration + +import com.pixelized.server.lwa.server.Engine +import io.ktor.server.response.respond + +fun Engine.getAlteration(): suspend io.ktor.server.routing.RoutingContext.() -> Unit { + return { + call.respond(alterationService.alterations()) + } +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/alteration/PUT_ActiveAlteration.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/alteration/PUT_ActiveAlteration.kt new file mode 100644 index 0000000..b247518 --- /dev/null +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/alteration/PUT_ActiveAlteration.kt @@ -0,0 +1,63 @@ +package com.pixelized.server.lwa.server.rest.alteration + +import com.pixelized.server.lwa.server.Engine +import com.pixelized.shared.lwa.model.campaign.Campaign +import com.pixelized.shared.lwa.protocol.websocket.Message +import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation +import io.ktor.http.HttpStatusCode +import io.ktor.server.request.receive +import io.ktor.server.response.respondText + +fun Engine.putActiveAlteration(): suspend io.ktor.server.routing.RoutingContext.() -> Unit { + return { + // fetch the query parameters + val characterSheetId = call.queryParameters["characterSheetId"] + val instanceId = call.queryParameters["instanceId"]?.toIntOrNull() + val alterationId = call.receive() + + // build the characterInstanceId from the parameters + val characterInstanceId = if (characterSheetId != null && instanceId != null) { + Campaign.CharacterInstance.Id( + characterSheetId = characterSheetId, + instanceId = instanceId + ) + } else { + null + } + + // Update the alteration + val updated = characterInstanceId?.let { + alterationService.toggleActiveAlteration( + characterInstanceId = it, + alterationId = alterationId, + ) + } ?: false + + // build the Http response & send it + val code = when (updated) { + true -> HttpStatusCode.Accepted + else -> HttpStatusCode.UnprocessableEntity + } + call.respondText( + text = "$code", + status = code, + ) + + // share the modification to all client through the websocket. + characterInstanceId?.let { + webSocket.emit( + Message( + from = "Server", + value = RestSynchronisation.ToggleActiveAlteration( + characterId = campaignJsonFactory.convertToJson(id = it), + alterationId = alterationId, + active = alterationService.isAlterationActive( + characterInstanceId = it, + alterationId = alterationId + ), + ), + ) + ) + } + } +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/DELETE_Campaign_character.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/DELETE_Campaign_character.kt index 60ac43a..b2cdc7b 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/DELETE_Campaign_character.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/DELETE_Campaign_character.kt @@ -1,6 +1,7 @@ package com.pixelized.server.lwa.server.rest.campaign import com.pixelized.server.lwa.server.Engine +import com.pixelized.shared.lwa.model.campaign.Campaign import com.pixelized.shared.lwa.protocol.websocket.Message import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation import io.ktor.http.HttpStatusCode @@ -8,7 +9,16 @@ import io.ktor.server.response.respondText fun Engine.deleteCampaignCharacter(): suspend io.ktor.server.routing.RoutingContext.() -> Unit { return { - val id = call.queryParameters["id"] + val characterSheetId = call.queryParameters["characterSheetId"] + val instanceId = call.queryParameters["instanceId"]?.toIntOrNull() + val id = if (characterSheetId != null && instanceId != null) { + Campaign.CharacterInstance.Id( + characterSheetId = characterSheetId, + instanceId = instanceId + ) + } else { + null + } val updated = id?.let { campaignService.removeCharacter(it) } ?: false val code = when (updated) { true -> HttpStatusCode.Accepted diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/DELETE_Campaign_npc.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/DELETE_Campaign_npc.kt index b41781f..82edfb5 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/DELETE_Campaign_npc.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/DELETE_Campaign_npc.kt @@ -1,6 +1,7 @@ package com.pixelized.server.lwa.server.rest.campaign import com.pixelized.server.lwa.server.Engine +import com.pixelized.shared.lwa.model.campaign.Campaign import com.pixelized.shared.lwa.protocol.websocket.Message import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation import io.ktor.http.HttpStatusCode @@ -8,7 +9,16 @@ import io.ktor.server.response.respondText fun Engine.deleteCampaignNpc(): suspend io.ktor.server.routing.RoutingContext.() -> Unit { return { - val id = call.queryParameters["id"] + val characterSheetId = call.queryParameters["characterSheetId"] + val instanceId = call.queryParameters["instanceId"]?.toIntOrNull() + val id = if (characterSheetId != null && instanceId != null) { + Campaign.CharacterInstance.Id( + characterSheetId = characterSheetId, + instanceId = instanceId + ) + } else { + null + } val updated = id?.let { campaignService.removeNpc(it) } ?: false val code = when (updated) { true -> HttpStatusCode.Accepted diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/PUT_Campaign_character.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/PUT_Campaign_character.kt index 3d5d929..bd9f4a8 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/PUT_Campaign_character.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/PUT_Campaign_character.kt @@ -1,6 +1,7 @@ package com.pixelized.server.lwa.server.rest.campaign import com.pixelized.server.lwa.server.Engine +import com.pixelized.shared.lwa.model.campaign.Campaign import com.pixelized.shared.lwa.protocol.websocket.Message import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation import io.ktor.http.HttpStatusCode @@ -8,7 +9,16 @@ import io.ktor.server.response.respondText fun Engine.putCampaignCharacter(): suspend io.ktor.server.routing.RoutingContext.() -> Unit { return { - val id = call.queryParameters["id"] + val characterSheetId = call.queryParameters["characterSheetId"] + val instanceId = call.queryParameters["instanceId"]?.toIntOrNull() + val id = if (characterSheetId != null && instanceId != null) { + Campaign.CharacterInstance.Id( + characterSheetId = characterSheetId, + instanceId = instanceId + ) + } else { + null + } val updated = id?.let { campaignService.addCharacter(it) } ?: false val code = when (updated) { true -> HttpStatusCode.Accepted diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/PUT_Campaign_npc.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/PUT_Campaign_npc.kt index 1995107..4f73a89 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/PUT_Campaign_npc.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/PUT_Campaign_npc.kt @@ -1,6 +1,7 @@ package com.pixelized.server.lwa.server.rest.campaign import com.pixelized.server.lwa.server.Engine +import com.pixelized.shared.lwa.model.campaign.Campaign import com.pixelized.shared.lwa.protocol.websocket.Message import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation import io.ktor.http.HttpStatusCode @@ -8,16 +9,30 @@ import io.ktor.server.response.respondText fun Engine.putCampaignNpc(): suspend io.ktor.server.routing.RoutingContext.() -> Unit { return { - val id = call.queryParameters["id"] + val characterSheetId = call.queryParameters["characterSheetId"] + val instanceId = call.queryParameters["instanceId"]?.toIntOrNull() + + val id = if (characterSheetId != null && instanceId != null) { + Campaign.CharacterInstance.Id( + characterSheetId = characterSheetId, + instanceId = instanceId + ) + } else { + null + } + val updated = id?.let { campaignService.addNpc(it) } ?: false + val code = when (updated) { true -> HttpStatusCode.Accepted else -> HttpStatusCode.UnprocessableEntity } + call.respondText( text = "$code", status = code, ) + webSocket.emit( Message( from = "Server", diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/Module.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/Module.kt index f9060c9..97c75da 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/Module.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/Module.kt @@ -1,9 +1,17 @@ package com.pixelized.shared.lwa +import com.pixelized.shared.lwa.model.AlteredCharacterSheetFactory +import com.pixelized.shared.lwa.model.alteration.AlterationJsonFactory import com.pixelized.shared.lwa.model.campaign.CampaignJsonFactory import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJsonFactory +import com.pixelized.shared.lwa.parser.dice.DiceParser +import com.pixelized.shared.lwa.parser.expression.ExpressionParser +import com.pixelized.shared.lwa.parser.word.WordParser import com.pixelized.shared.lwa.usecase.CampaignUseCase import com.pixelized.shared.lwa.usecase.CharacterSheetUseCase +import com.pixelized.shared.lwa.usecase.ExpressionUseCase +import com.pixelized.shared.lwa.usecase.RollUseCase +import com.pixelized.shared.lwa.usecase.SkillStepUseCase import kotlinx.serialization.json.Json import org.koin.core.module.dsl.factoryOf import org.koin.dsl.module @@ -12,6 +20,7 @@ val sharedModuleDependencies get() = listOf( toolsDependencies, factoryDependencies, + parserDependencies, useCaseDependencies, ) @@ -29,10 +38,23 @@ val factoryDependencies get() = module { factoryOf(::CharacterSheetJsonFactory) factoryOf(::CampaignJsonFactory) + factoryOf(::AlteredCharacterSheetFactory) + factoryOf(::AlterationJsonFactory) + } + +val parserDependencies + get() = module { + factoryOf(::WordParser) + factoryOf(::DiceParser) + factoryOf(::ExpressionParser) } val useCaseDependencies get() = module { factoryOf(::CharacterSheetUseCase) factoryOf(::CampaignUseCase) + factoryOf(::SkillStepUseCase) + factoryOf(::RollUseCase) + factoryOf(::ExpressionUseCase) + factoryOf(::CharacterSheetUseCase) } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/StorePath.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/StorePath.kt index 2619ae3..9383029 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/StorePath.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/StorePath.kt @@ -43,4 +43,13 @@ fun campaignPath( OperatingSystem.Windows -> "${storePath(os = os)}campaign\\" OperatingSystem.Macintosh -> "${storePath(os = os)}campaign/" } +} + +fun alterationsPath( + os: OperatingSystem = OperatingSystem.current, +): String { + return when (os) { + OperatingSystem.Windows -> "${storePath(os = os)}alterations\\" + OperatingSystem.Macintosh -> "${storePath(os = os)}alterations/" + } } \ No newline at end of file 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 new file mode 100644 index 0000000..b3d20a8 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/AlteredCharacterSheet.kt @@ -0,0 +1,136 @@ +package com.pixelized.shared.lwa.model + +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.ARMOR +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.DMG +import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.GHP +import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.HEI +import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.HP +import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.INT +import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.LB +import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.LVL +import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.MOV +import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.PORTRAIT +import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.POW +import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.PP +import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.STR +import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.THUMBNAIL +import com.pixelized.shared.lwa.parser.expression.Expression +import com.pixelized.shared.lwa.usecase.CharacterSheetUseCase +import com.pixelized.shared.lwa.usecase.ExpressionUseCase + +class AlteredCharacterSheetFactory( + private val sheetUseCase: CharacterSheetUseCase, + private val expressionUseCase: ExpressionUseCase, +) { + fun sheet( + characterSheet: CharacterSheet, + alterations: Map>, + ): AlteredCharacterSheet { + return AlteredCharacterSheet( + sheetUseCase = sheetUseCase, + expressionUseCase = expressionUseCase, + sheet = characterSheet, + alterations = alterations, + ) + } +} + +class AlteredCharacterSheet( + private val sheetUseCase: CharacterSheetUseCase, + private val expressionUseCase: ExpressionUseCase, + private val sheet: CharacterSheet, + private val alterations: Map>, +) { + val id: String = sheet.id + + val name: String = sheet.name + + val portrait: String? + get() = alterations[PORTRAIT] + ?.firstNotNullOfOrNull { (it.expression as? Expression.UrlExpression)?.url } + ?: sheet.portrait + + val thumbnail: String? + get() = alterations[THUMBNAIL] + ?.firstNotNullOfOrNull { (it.expression as? Expression.UrlExpression)?.url } + ?: sheet.thumbnail + + val level: Int + get() = sheet.level + alterations[LVL].sum() + + val strength: Int + get() = sheet.strength + alterations[STR].sum() + + val dexterity: Int + get() = sheet.dexterity + alterations[DEX].sum() + + val constitution: Int + get() = sheet.constitution + alterations[CON].sum() + + val height: Int + get() = sheet.height + alterations[HEI].sum() + + val intelligence: Int + get() = sheet.intelligence + alterations[INT].sum() + + val power: Int + get() = sheet.power + alterations[POW].sum() + + val charisma: Int + get() = sheet.charisma + alterations[CHA].sum() + + val movement: Int + get() = sheetUseCase.movement() + alterations[MOV].sum() + + val armor: Int + get() = sheetUseCase.armor() + alterations[ARMOR].sum() + + val maxHp: Int + get() = sheetUseCase.maxHp( + constitution = constitution, + height = height, + level = level, + ) + alterations[HP].sum() + + val maxPp: Int + get() = sheetUseCase.maxPp( + power = power, + ) + alterations[PP].sum() + + val damageBonus: String + get() { + val initial = sheetUseCase.damageBonus( + strength = strength, + height = height, + ) + return alterations[DMG] + ?.joinToString(separator = "+") { it.expression.toString() } + ?.let { "$initial+$it" } + ?: initial + } + + val learning: Int + get() = sheetUseCase.learning( + intelligence = intelligence + ) + alterations[LB].sum() + + val hpGrow: Int + get() = sheetUseCase.hpGrow( + constitution = constitution, + ) + alterations[GHP].sum() + + // Helper method + + private fun List?.sum() = this?.sumOf { + expressionUseCase.computeExpression( + sheet = sheet, + alterations = alterations, + expression = it.expression + ) + } ?: 0 +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/alteration/Alteration.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/alteration/Alteration.kt new file mode 100644 index 0000000..85132f3 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/alteration/Alteration.kt @@ -0,0 +1,19 @@ +package com.pixelized.shared.lwa.model.alteration + +import com.pixelized.shared.lwa.parser.expression.Expression + +data class Alteration( + val id: String, + val metadata: MetaData, + val fields: List, +) { + data class MetaData( + val name: String, + val description: String, + ) + + data class Field( + val fieldId: String, // this id is not the id of the instance but the id of the impacted field (characteristic, skill etc.) + val expression: Expression, + ) +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/alteration/AlterationJson.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/alteration/AlterationJson.kt new file mode 100644 index 0000000..aeeb465 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/alteration/AlterationJson.kt @@ -0,0 +1,6 @@ +package com.pixelized.shared.lwa.model.alteration + +import kotlinx.serialization.Serializable + +@Serializable +sealed interface AlterationJson \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/alteration/AlterationJsonFactory.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/alteration/AlterationJsonFactory.kt new file mode 100644 index 0000000..e86b9b1 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/alteration/AlterationJsonFactory.kt @@ -0,0 +1,73 @@ +package com.pixelized.shared.lwa.model.alteration + +import com.pixelized.shared.lwa.parser.expression.Expression +import com.pixelized.shared.lwa.parser.expression.ExpressionParser + +class AlterationJsonFactory( + private val expressionParser: ExpressionParser, +) { + + fun convertFromJson( + json: AlterationJson, + ): Alteration { + return when (json) { + is AlterationJsonV1 -> convertFromJsonV1(json = json) + } + } + + fun convertFromJsonV1( + json: AlterationJsonV1, + ): Alteration { + return Alteration( + id = json.id, + metadata = convertFromJsonV1(json = json.metadata), + fields = json.fields.map { convertFromJsonV1(json = it) } + ) + } + + fun convertFromJsonV1( + json: AlterationJsonV1.AlterationMetadataJsonV1, + ): Alteration.MetaData { + return Alteration.MetaData( + name = json.name, + description = json.description, + ) + } + + fun convertFromJsonV1( + json: AlterationJsonV1.FieldJsonV1, + ): Alteration.Field { + return Alteration.Field( + fieldId = json.fieldId, + expression = expressionParser.parse(json.expression) ?: Expression.Flat(0), + ) + } + + fun convertToJson( + data: Alteration, + ): AlterationJson { + return AlterationJsonV1( + id = data.id, + metadata = convertToJson(data = data.metadata), + fields = data.fields.map { convertToJson(data = it) }, + ) + } + + fun convertToJson( + data: Alteration.MetaData, + ): AlterationJsonV1.AlterationMetadataJsonV1 { + return AlterationJsonV1.AlterationMetadataJsonV1( + name = data.name, + description = data.description, + ) + } + + fun convertToJson( + data: Alteration.Field, + ): AlterationJsonV1.FieldJsonV1 { + return AlterationJsonV1.FieldJsonV1( + fieldId = data.fieldId, + expression = data.expression.toString(), + ) + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/alteration/AlterationJsonV1.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/alteration/AlterationJsonV1.kt new file mode 100644 index 0000000..dbdd13a --- /dev/null +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/alteration/AlterationJsonV1.kt @@ -0,0 +1,23 @@ +package com.pixelized.shared.lwa.model.alteration + +import kotlinx.serialization.Serializable + +@Serializable +data class AlterationJsonV1( + val id: String, + val metadata: AlterationMetadataJsonV1, + val fields: List, +) : AlterationJson { + + @Serializable + data class FieldJsonV1( + val fieldId: String, // this id is not the id of the instance but the id of the impacted field (characteristic, skill etc.) + val expression: String, + ) + + @Serializable + data class AlterationMetadataJsonV1( + val name: String, + val description: String, + ) +} diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/alteration/FieldAlteration.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/alteration/FieldAlteration.kt new file mode 100644 index 0000000..b9c6331 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/alteration/FieldAlteration.kt @@ -0,0 +1,9 @@ +package com.pixelized.shared.lwa.model.alteration + +import com.pixelized.shared.lwa.parser.expression.Expression + +data class FieldAlteration( + val alterationId: String, + val metadata: Alteration.MetaData, + val expression: Expression, +) \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/Campaign.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/Campaign.kt index 54524fb..8ac614d 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/Campaign.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/Campaign.kt @@ -1,12 +1,17 @@ package com.pixelized.shared.lwa.model.campaign data class Campaign( - val characters: Map, - val npcs: Map, + val characters: Map, + val npcs: Map, ) { data class CharacterInstance( val characteristic: Map, ) { + data class Id( + val characterSheetId: String, + val instanceId: Int, + ) + enum class Characteristic { Damage, Power, @@ -21,12 +26,18 @@ data class Campaign( } } -fun Campaign.character(id: String): Campaign.CharacterInstance { +fun Campaign.character(id: Campaign.CharacterInstance.Id): Campaign.CharacterInstance { return characters[id] ?: Campaign.CharacterInstance( characteristic = emptyMap(), ) } +fun Campaign.npc(id: Campaign.CharacterInstance.Id): Campaign.CharacterInstance { + return npcs[id] ?: Campaign.CharacterInstance( + characteristic = emptyMap(), + ) +} + val Campaign.CharacterInstance.level get() = characteristic[Campaign.CharacterInstance.Characteristic.Damage] ?: 1 diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/CampaignJsonFactory.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/CampaignJsonFactory.kt index 9bd1acc..84b3e66 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/CampaignJsonFactory.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/CampaignJsonFactory.kt @@ -6,51 +6,94 @@ class CampaignJsonFactory { json: CampaignJson, ): Campaign { return when (json) { - is CampaignJsonV1 -> convertFromV1(json = json) + is CampaignJsonV1 -> convertFromV1(campaignJson = json) } } private fun convertFromV1( - json: CampaignJsonV1, + campaignJson: CampaignJsonV1, ): Campaign { return Campaign( - characters = json.characters.mapValues { convertFromV1(json = it.value) }, - npcs = json.npcs.mapValues { convertFromV1(json = it.value) }, + characters = campaignJson.characters + .map { + convertFromV1(characterInstanceIdJson = it.key) to convertFromV1( + characterInstanceJson = it.value + ) + } + .toMap(), + npcs = campaignJson.npcs + .map { + convertFromV1(characterInstanceIdJson = it.key) to convertFromV1( + characterInstanceJson = it.value + ) + } + .toMap(), ) } - private fun convertFromV1( - json: CampaignJsonV1.CharacterInstanceJson, + fun convertFromV1( + characterInstanceIdJson: String, + ): Campaign.CharacterInstance.Id { + return Campaign.CharacterInstance.Id( + characterSheetId = characterInstanceIdJson.drop(4), // drop first 3 number then the - + instanceId = characterInstanceIdJson.take(3).toIntOrNull() ?: 0, + ) + } + + fun convertFromV1( + characterInstanceJson: CampaignJsonV1.CharacterInstanceJson, ): Campaign.CharacterInstance { return Campaign.CharacterInstance( - characteristic = json.characteristic.map { char -> - when (char.key) { - CampaignJsonV1.CharacterInstanceJson.Characteristic.Damage -> Campaign.CharacterInstance.Characteristic.Damage - CampaignJsonV1.CharacterInstanceJson.Characteristic.Power -> Campaign.CharacterInstance.Characteristic.Power - } to char.value - }.toMap(), + characteristic = characterInstanceJson.characteristic + .map { char -> convertFromV1(characteristicJson = char.key) to char.value } + .toMap(), ) } + fun convertFromV1( + characteristicJson: CampaignJsonV1.CharacterInstanceJson.Characteristic, + ): Campaign.CharacterInstance.Characteristic { + return when (characteristicJson) { + CampaignJsonV1.CharacterInstanceJson.Characteristic.Damage -> Campaign.CharacterInstance.Characteristic.Damage + CampaignJsonV1.CharacterInstanceJson.Characteristic.Power -> Campaign.CharacterInstance.Characteristic.Power + } + } + fun convertToJson( data: Campaign, ): CampaignJson { return CampaignJsonV1( - characters = data.characters.mapValues { convertToJson(data = it.value) }, - npcs = data.npcs.mapValues { convertToJson(data = it.value) }, + characters = data.characters + .map { convertToJson(id = it.key) to convertToJson(data = it.value) } + .toMap(), + npcs = data.npcs + .map { convertToJson(id = it.key) to convertToJson(data = it.value) } + .toMap(), ) } - private fun convertToJson( + fun convertToJson( + id: Campaign.CharacterInstance.Id, + ): String { + return "${String.format("%03d", id.instanceId)}-${id.characterSheetId}" + } + + fun convertToJson( data: Campaign.CharacterInstance, ): CampaignJsonV1.CharacterInstanceJson { return CampaignJsonV1.CharacterInstanceJson( - characteristic = data.characteristic.map { char -> - when (char.key) { - Campaign.CharacterInstance.Characteristic.Damage -> CampaignJsonV1.CharacterInstanceJson.Characteristic.Damage - Campaign.CharacterInstance.Characteristic.Power -> CampaignJsonV1.CharacterInstanceJson.Characteristic.Power - } to char.value - }.toMap(), + characteristic = data.characteristic + .map { char -> convertToJson(characteristic = char.key) to char.value } + .toMap(), ) } + + fun convertToJson( + characteristic: Campaign.CharacterInstance.Characteristic, + ): CampaignJsonV1.CharacterInstanceJson.Characteristic { + return when (characteristic) { + Campaign.CharacterInstance.Characteristic.Damage -> CampaignJsonV1.CharacterInstanceJson.Characteristic.Damage + Campaign.CharacterInstance.Characteristic.Power -> CampaignJsonV1.CharacterInstanceJson.Characteristic.Power + } + } } \ No newline at end of file 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 73816ff..95a9284 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 @@ -16,12 +16,7 @@ data class CharacterSheet( val charisma: Int, // sub characteristics val movement: Int, - val hp: Int, - val pp: Int, - val damageBonus: String, val armor: Int, - val learning: Int, - val hpGrow: Int, // skills val commonSkills: List, val specialSkills: List, 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 ecc07fa..bc9cc9f 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 @@ -31,26 +31,8 @@ class CharacterSheetJsonFactory( intelligence = json.intelligence, power = json.power, charisma = json.charisma, - movement = defaultMovement(), - hp = defaultMaxHp( - constitution = json.constitution, - height = json.height, - level = json.level - ), - pp = defaultMaxPower( - power = json.power, - ), - damageBonus = defaultDamageBonus( - strength = json.strength, - height = json.height, - ), - armor = defaultArmor(), - learning = defaultLearning( - intelligence = json.intelligence, - ), - hpGrow = defaultHpGrow( - constitution = json.constitution, - ), + movement = movement(), + armor = armor(), commonSkills = json.skills.map { CharacterSheet.Skill( id = it.id, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/parser/dice/Dice.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/parser/dice/Dice.kt similarity index 91% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/parser/dice/Dice.kt rename to shared/src/commonMain/kotlin/com/pixelized/shared/lwa/parser/dice/Dice.kt index d5a71aa..3bdf43f 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/parser/dice/Dice.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/parser/dice/Dice.kt @@ -1,4 +1,4 @@ -package com.pixelized.desktop.lwa.parser.dice +package com.pixelized.shared.lwa.parser.dice data class Dice( val modifier: Modifier?, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/parser/dice/DiceParser.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/parser/dice/DiceParser.kt similarity index 94% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/parser/dice/DiceParser.kt rename to shared/src/commonMain/kotlin/com/pixelized/shared/lwa/parser/dice/DiceParser.kt index 708c69e..12550bf 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/parser/dice/DiceParser.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/parser/dice/DiceParser.kt @@ -1,4 +1,4 @@ -package com.pixelized.desktop.lwa.parser.dice +package com.pixelized.shared.lwa.parser.dice class DiceParser { private val diceParser = Regex( diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/parser/expression/Expression.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/parser/expression/Expression.kt similarity index 82% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/parser/expression/Expression.kt rename to shared/src/commonMain/kotlin/com/pixelized/shared/lwa/parser/expression/Expression.kt index 14b9c4b..5281752 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/parser/expression/Expression.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/parser/expression/Expression.kt @@ -1,7 +1,7 @@ -package com.pixelized.desktop.lwa.parser.expression +package com.pixelized.shared.lwa.parser.expression -import com.pixelized.desktop.lwa.parser.dice.Dice -import com.pixelized.desktop.lwa.parser.word.Word +import com.pixelized.shared.lwa.parser.dice.Dice +import com.pixelized.shared.lwa.parser.word.Word sealed interface Expression { @@ -10,7 +10,7 @@ sealed interface Expression { val second: Expression?, ) : Expression { override fun toString(): String { - return "$first+$second" + return "($first+$second)" } } @@ -19,7 +19,7 @@ sealed interface Expression { val second: Expression?, ) : Expression { override fun toString(): String { - return "$first-$second" + return "($first-$second)" } } @@ -28,7 +28,7 @@ sealed interface Expression { val second: Expression?, ) : Expression { override fun toString(): String { - return "$first/$second" + return "($first/$second)" } } @@ -37,7 +37,7 @@ sealed interface Expression { val second: Expression?, ) : Expression { override fun toString(): String { - return "$first*$second" + return "($first*$second)" } } @@ -46,7 +46,7 @@ sealed interface Expression { val second: Expression?, ) : Expression { override fun toString(): String { - return "minimum($first,$second)" + return "min($first,$second)" } } @@ -55,7 +55,7 @@ sealed interface Expression { val second: Expression?, ) : Expression { override fun toString(): String { - return "maximum($first,$second)" + return "max($first,$second)" } } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/parser/expression/ExpressionParser.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/parser/expression/ExpressionParser.kt similarity index 97% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/parser/expression/ExpressionParser.kt rename to shared/src/commonMain/kotlin/com/pixelized/shared/lwa/parser/expression/ExpressionParser.kt index 9ad9d6e..107c671 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/parser/expression/ExpressionParser.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/parser/expression/ExpressionParser.kt @@ -1,10 +1,8 @@ -package com.pixelized.desktop.lwa.parser.expression +package com.pixelized.shared.lwa.parser.expression -import com.pixelized.desktop.lwa.parser.dice.DiceParser -import com.pixelized.desktop.lwa.parser.word.WordParser -import org.jetbrains.skia.toIPoint +import com.pixelized.shared.lwa.parser.dice.DiceParser +import com.pixelized.shared.lwa.parser.word.WordParser import java.net.URI -import java.net.URL /** * Highly inspired by the following javascript implementation: @@ -199,7 +197,6 @@ class ExpressionParser( } val url = try { - println(token) URI.create(token).toString() } catch (_: Exception) { null diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/parser/word/Word.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/parser/word/Word.kt similarity index 89% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/parser/word/Word.kt rename to shared/src/commonMain/kotlin/com/pixelized/shared/lwa/parser/word/Word.kt index b207c3f..8e9d20c 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/parser/word/Word.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/parser/word/Word.kt @@ -1,4 +1,4 @@ -package com.pixelized.desktop.lwa.parser.word +package com.pixelized.shared.lwa.parser.word data class Word( val type: Type, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/parser/word/WordParser.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/parser/word/WordParser.kt similarity index 82% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/parser/word/WordParser.kt rename to shared/src/commonMain/kotlin/com/pixelized/shared/lwa/parser/word/WordParser.kt index bd1f749..249f650 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/parser/word/WordParser.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/parser/word/WordParser.kt @@ -1,4 +1,4 @@ -package com.pixelized.desktop.lwa.parser.word +package com.pixelized.shared.lwa.parser.word class WordParser { diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/payload/RestSynchronisation.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/payload/RestSynchronisation.kt index de1bfee..5d7be8c 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/payload/RestSynchronisation.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/payload/RestSynchronisation.kt @@ -15,6 +15,13 @@ sealed class RestSynchronisation : MessagePayload { val characterId: String, ) : RestSynchronisation() + @Serializable + data class ToggleActiveAlteration( + val characterId: String, + val alterationId: String, + val active: Boolean, + ) : RestSynchronisation() + @Serializable data object Campaign : RestSynchronisation() } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/payload/UpdatePlayerCharacteristicMessage.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/payload/UpdatePlayerCharacteristicMessage.kt index ef04b78..50a9f00 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/payload/UpdatePlayerCharacteristicMessage.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/payload/UpdatePlayerCharacteristicMessage.kt @@ -1,11 +1,11 @@ package com.pixelized.shared.lwa.protocol.websocket.payload -import com.pixelized.shared.lwa.model.campaign.Campaign +import com.pixelized.shared.lwa.model.campaign.CampaignJsonV1 import kotlinx.serialization.Serializable @Serializable data class UpdatePlayerCharacteristicMessage( - val characterId: String, - val characteristic: Campaign.CharacterInstance.Characteristic, + val characterInstanceId: String, + val characteristic: CampaignJsonV1.CharacterInstanceJson.Characteristic, val value: Int, ) : MessagePayload \ No newline at end of file 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 55dead3..aaa09b2 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 @@ -10,31 +10,31 @@ class CharacterSheetUseCase { return value - value % 5 // (truncate(value.toFloat() / 5f) * 5f).toInt() } - fun defaultMovement(): Int = 10 + fun movement(): Int = 10 - fun defaultMaxHp( + fun maxHp( constitution: Int, height: Int, level: Int, ): Int { - val add = max(defaultHpGrow(constitution = constitution) * (level - 1), 0) + val add = max(hpGrow(constitution = constitution) * (level - 1), 0) return (ceil((constitution + height) / 2f).toInt()) + add } - fun defaultMaxPower( + fun maxPp( power: Int, ): Int { return power } - fun defaultDamageBonus( + fun damageBonus( strength: Int, height: Int, ): String { - return defaultDamageBonus(sum = strength + height) + return damageBonus(sum = strength + height) } - fun defaultDamageBonus( + fun damageBonus( sum: Int, ): String { return when { @@ -47,13 +47,13 @@ class CharacterSheetUseCase { } } - fun defaultArmor(): Int = 0 + fun armor(): Int = 0 - fun defaultLearning(intelligence: Int): Int { + fun learning(intelligence: Int): Int { return max(0, (intelligence - 10) * 2) } - fun defaultHpGrow(constitution: Int): Int { + fun hpGrow(constitution: Int): Int { return (constitution / 3) } 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 new file mode 100644 index 0000000..0589a33 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/usecase/ExpressionUseCase.kt @@ -0,0 +1,172 @@ +package com.pixelized.shared.lwa.usecase + + +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 +import kotlin.math.max +import kotlin.math.min + +class ExpressionUseCase( + private val expressionParser: ExpressionParser, + private val characterSheetUseCase: CharacterSheetUseCase, + private val rollUseCase: RollUseCase, +) { + fun computeSkillValue( + sheet: CharacterSheet, + alterations: Map>, + skill: CharacterSheet.Skill, + ): Int { + val context = Context( + sheet = sheet, + skill = skill, + alterations = alterations, + ) + + val base: Int = context.evaluate( + expression = skill.base.let(expressionParser::parse), + ).let { + when (skill.occupation) { + true -> max(MIN_OCCUPATION_VALUE, it) + else -> it + } + } + + val bonus = context.evaluate( + expression = skill.bonus?.let(expressionParser::parse), + ) + + val level = max((skill.level - 1) * 5, 0) + + val alteration = alterations[skill.id]?.sumOf { + context.evaluate(it.expression) + } ?: 0 + + return max(base + bonus + level + alteration, 0) + } + + fun computeRoll( + sheet: CharacterSheet, + alterations: Map>, + expression: String, + ): Int { + return expressionParser.parse(input = expression)?.let { + computeExpression( + sheet = sheet, + alterations = alterations, + expression = it, + ) + } ?: 0 + } + + fun computeExpression( + sheet: CharacterSheet, + alterations: Map>, + expression: Expression, + ): Int { + val context = Context( + sheet = sheet, + skill = null, + alterations = alterations, + ) + return context.evaluate( + expression = expression, + ) + } + + private fun Context.evaluate(expression: Expression?): Int { + + fun List?.sum() = this?.sumOf { evaluate(it.expression) } ?: 0 + + return when (expression) { + is Expression.Add -> { + evaluate(expression.first) + evaluate(expression.second) + } + + is Expression.Minus -> { + evaluate(expression.first) - evaluate(expression.second) + } + + is Expression.Div -> { + evaluate(expression.first) / evaluate(expression.second) + } + + is Expression.Prod -> { + evaluate(expression.first) * evaluate(expression.second) + } + + is Expression.Inversion -> { + -evaluate(expression.expression) + } + + is Expression.Maximum -> { + min(evaluate(expression.first), evaluate(expression.second)) + } + + is Expression.Minimum -> { + max(evaluate(expression.first), evaluate(expression.second)) + } + + is Expression.Flat -> { + expression.value + } + + is Expression.DiceExpression -> { + rollUseCase.roll(expression.dice) + } + + is Expression.UrlExpression -> { + 0 // Ignore this case. + } + + 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(), + ) + ) + ) + + Word.Type.BDD -> evaluate( + expression = expressionParser.parse( + characterSheetUseCase.damageBonus( + strength = sheet.strength + alterations[STR].sum(), + height = sheet.height + alterations[HEI].sum(), + ) + ) + ) + + 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() + } + + null -> 0 + } + } + + data class Context( + val sheet: CharacterSheet, + val skill: CharacterSheet.Skill?, + val alterations: Map>, + ) + + companion object { + private const val MIN_OCCUPATION_VALUE = 40 + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/business/RollUseCase.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/usecase/RollUseCase.kt similarity index 95% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/business/RollUseCase.kt rename to shared/src/commonMain/kotlin/com/pixelized/shared/lwa/usecase/RollUseCase.kt index bf2ed48..f7e1bf2 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/business/RollUseCase.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/usecase/RollUseCase.kt @@ -1,7 +1,7 @@ -package com.pixelized.desktop.lwa.business +package com.pixelized.shared.lwa.usecase -import com.pixelized.desktop.lwa.parser.dice.Dice +import com.pixelized.shared.lwa.parser.dice.Dice import kotlin.math.abs import kotlin.math.max import kotlin.math.min diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/business/SkillStepUseCase.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/usecase/SkillStepUseCase.kt similarity index 95% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/business/SkillStepUseCase.kt rename to shared/src/commonMain/kotlin/com/pixelized/shared/lwa/usecase/SkillStepUseCase.kt index 203f8ac..94a5e51 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/business/SkillStepUseCase.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/usecase/SkillStepUseCase.kt @@ -1,9 +1,9 @@ -package com.pixelized.desktop.lwa.business +package com.pixelized.shared.lwa.usecase -import androidx.compose.ui.util.fastRoundToInt import kotlin.math.max import kotlin.math.min import kotlin.math.round +import kotlin.math.roundToInt class SkillStepUseCase { @@ -35,7 +35,7 @@ class SkillStepUseCase { ) } - private inline fun roundToInt(block: () -> Float): Int = round(block()).fastRoundToInt() + private inline fun roundToInt(block: () -> Float): Int = round(block()).roundToInt() fun exportWiki() { fun print(range: IntRange): String = when {