Add the alteration system to the server & link the app on it.
This commit is contained in:
parent
4ed11660c3
commit
29747dcb5c
83 changed files with 1797 additions and 811 deletions
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<AlterationJson>
|
||||
|
||||
suspend fun activeAlterations(characterSheetId: String, instanceId: Int): List<String>
|
||||
|
||||
suspend fun toggleActiveAlterations(
|
||||
characterSheetId: String,
|
||||
instanceId: Int,
|
||||
alterationId: String,
|
||||
)
|
||||
}
|
||||
|
|
@ -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<Unit>()
|
||||
|
||||
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<Unit>()
|
||||
|
||||
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<Unit>()
|
||||
|
||||
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<Unit>()
|
||||
|
||||
override suspend fun alterations(): List<AlterationJson> = client
|
||||
.get("$root/alterations")
|
||||
.body()
|
||||
|
||||
override suspend fun activeAlterations(
|
||||
characterSheetId: String,
|
||||
instanceId: Int,
|
||||
): List<String> = 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<Unit>()
|
||||
}
|
||||
|
|
@ -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<CharacterId, MutableStateFlow<List<AlterationId>>> =
|
||||
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<Map<Campaign.CharacterInstance.Id, Map<String, List<FieldAlteration>>>> =
|
||||
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<Map<String, List<FieldAlteration>>> {
|
||||
return activeAlterationIdMapFlow
|
||||
.getOrPut(characterId) { MutableStateFlow(emptyList()) }
|
||||
.map { activeAlterationIds ->
|
||||
val fieldAlterations = hashMapOf<String, MutableList<FieldAlteration>>()
|
||||
|
||||
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<Map<String, List<FieldAlteration>>> {
|
||||
return activeAlterationMapFlow.map { it[characterId] ?: emptyMap() }
|
||||
}
|
||||
|
||||
fun toggle(characterId: String, alterationId: String) {
|
||||
fun alterations(
|
||||
characterInstanceId: Campaign.CharacterInstance.Id,
|
||||
): Map<String, List<FieldAlteration>> {
|
||||
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<String, Alteration>,
|
||||
actives: List<String>,
|
||||
): Map<String, List<FieldAlteration>> {
|
||||
val fieldAlterations = hashMapOf<String, MutableList<FieldAlteration>>()
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Map<String, Alteration>>(emptyMap())
|
||||
val alterations: StateFlow<Map<String, Alteration>> = _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<Map<CharacterInstance.Id, List<String>>>(emptyMap())
|
||||
val active: StateFlow<Map<CharacterInstance.Id, List<String>>> 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<String, Alteration> {
|
||||
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<String> {
|
||||
val request = client.activeAlterations(
|
||||
characterSheetId = characterInstanceId.characterSheetId,
|
||||
instanceId = characterInstanceId.instanceId,
|
||||
)
|
||||
)
|
||||
_active.value = _active.value.toMutableMap().also {
|
||||
it[characterInstanceId] = request
|
||||
}
|
||||
return request
|
||||
}
|
||||
|
||||
fun alterations(): Collection<Alteration> = alterations.values
|
||||
fun alterations(): Collection<Alteration> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Field>,
|
||||
) {
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
package com.pixelized.desktop.lwa.repository.alteration.model
|
||||
|
||||
data class AlterationMetadata(
|
||||
val name: String,
|
||||
val description: String,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -17,7 +17,9 @@ class CampaignRepository(
|
|||
|
||||
val campaignFlow get() = store.campaignFlow
|
||||
|
||||
fun characterInstanceFlow(id: String): StateFlow<Campaign.CharacterInstance> {
|
||||
fun characterInstanceFlow(
|
||||
id: Campaign.CharacterInstance.Id,
|
||||
): StateFlow<Campaign.CharacterInstance> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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<CharacterSheetCharacteristicDialogUio?>(null)
|
||||
val statChangeDialog: State<CharacterSheetCharacteristicDialogUio?> 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<String, List<FieldAlteration>> = 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
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<StatChangeDialogUio?>,
|
||||
onConfirm: (StatChangeDialogUio) -> Unit,
|
||||
fun CharacterSheetCharacteristicDialog(
|
||||
dialog: State<CharacterSheetCharacteristicDialogUio?>,
|
||||
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
|
||||
|
|
@ -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<String, List<FieldAlteration>>,
|
||||
): 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}",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String>(CHARACTER_ID) ?: error("missing character id")
|
||||
characterInstanceId = Campaign.CharacterInstance.Id(
|
||||
savedStateHandle.get<String>(CHARACTER_SHEET_ID) ?: error("missing character id"),
|
||||
savedStateHandle.get<Int>(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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ fun CampaignScreen(
|
|||
modifier = Modifier
|
||||
.padding(all = 8.dp)
|
||||
.fillMaxHeight(),
|
||||
viewModel = characterDetailViewModel,
|
||||
detailViewModel = characterDetailViewModel,
|
||||
dismissedViewModel = dismissedViewModel,
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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<CharacterDetailHeaderInstanceUio?>,
|
||||
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<CharacterDetailHeaderInstanceUio?>,
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -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<String, List<FieldAlteration>>,
|
||||
): CharacterDetailHeaderUio? {
|
||||
if (sheet == null) return null
|
||||
if (characterSheet == null) return null
|
||||
|
||||
fun List<FieldAlteration>?.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}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String?>(null)
|
||||
private val displayedCharacterId = MutableStateFlow<Campaign.CharacterInstance.Id?>(null)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val detail: StateFlow<CharacterDetailHeaderUio?> = 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<CharacterDetailHeaderUio?> = 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<CharacterDetailHeaderInstanceUio?> {
|
||||
val scope = rememberCoroutineScope()
|
||||
val flow: StateFlow<CharacterDetailHeaderInstanceUio?> = 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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<DiminishedStatDialogUio?>(null)
|
||||
val diminishedDialog: State<DiminishedStatDialogUio?> 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,
|
||||
// )
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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<String, List<FieldAlteration>>,
|
||||
): PlayerPortraitUio? {
|
||||
if (characterSheet == null) return null
|
||||
|
||||
fun List<FieldAlteration>?.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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<PlayerPortraitUio?, List<PlayerPortraitUio>>(
|
||||
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<PlayerPortraitRollUio?> {
|
||||
val state = rolls.getOrPut(characterId) { mutableStateOf(null) }
|
||||
LaunchedEffect(characterId) {
|
||||
fun roll(characterSheetId: String): State<PlayerPortraitRollUio?> {
|
||||
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 }
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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<String, List<FieldAlteration>>,
|
||||
): CharacterSheetPageUio? {
|
||||
if (sheet == null) return null
|
||||
if (characterSheet == null) return null
|
||||
|
||||
fun List<FieldAlteration>?.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,
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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<Boolean> get() = _displayRollOverlay
|
||||
|
||||
private val _statChangeDialog = mutableStateOf<StatChangeDialogUio?>(null)
|
||||
val statChangeDialog: State<StatChangeDialogUio?> get() = _statChangeDialog
|
||||
|
||||
private val _diminishedDialog = mutableStateOf<DiminishedStatDialogUio?>(null)
|
||||
val diminishedDialog: State<DiminishedStatDialogUio?> 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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<List<CharacterUio>>
|
||||
@Composable
|
||||
get() = repository
|
||||
.characterSheetPreviewFlow
|
||||
.collectAsState { sheets ->
|
||||
sheets.map { sheet ->
|
||||
CharacterUio(
|
||||
id = sheet.id,
|
||||
name = sheet.name,
|
||||
)
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val characters: StateFlow<List<CharacterUio>> = 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<Boolean>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
@ -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'"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.pixelized.desktop.lwa.business
|
||||
|
||||
import com.pixelized.shared.lwa.usecase.RollUseCase
|
||||
import org.junit.Test
|
||||
|
||||
class RollUseCaseTest {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<String, List<String>> ->
|
||||
campaignJsonFactory.convertFromV1(characterInstanceIdJson = it.key)
|
||||
}
|
||||
}.stateIn(
|
||||
scope = scope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = emptyMap()
|
||||
)
|
||||
|
||||
fun alterations(): List<AlterationJson> {
|
||||
return alterations.value
|
||||
}
|
||||
|
||||
fun active(
|
||||
characterInstanceId: Campaign.CharacterInstance.Id,
|
||||
): List<String> {
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<List<AlterationJson>>(emptyList())
|
||||
private val activeFlow = MutableStateFlow<Map<String, List<String>>>(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<List<AlterationJson>> = alterationsFlow
|
||||
|
||||
fun activeFlow(): StateFlow<Map<String, List<String>>> = activeFlow
|
||||
|
||||
private fun updateAlterations() {
|
||||
alterationsFlow.value = loadAlterations()
|
||||
}
|
||||
|
||||
private fun updateActiveAlterations() {
|
||||
activeFlow.value = loadActiveAlterations()
|
||||
}
|
||||
|
||||
private fun loadAlterations(): List<AlterationJson> {
|
||||
return try {
|
||||
val alterationFile = file()
|
||||
val json = alterationFile.readText(charset = Charsets.UTF_8)
|
||||
if (json.isBlank()) error("alterations file is empty")
|
||||
this.json.decodeFromString<List<AlterationJson>>(json)
|
||||
} catch (exception: Exception) {
|
||||
// TODO log exception
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadActiveAlterations(): Map<String, List<String>> {
|
||||
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<List<String>>(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<String>.toggle(alterationId: String): MutableList<String> {
|
||||
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)
|
||||
}
|
||||
|
|
@ -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<CampaignJson> = 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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Campaign> = 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<CampaignJson>(json)
|
||||
factory.convertFromJson(campaign)
|
||||
} catch (exception: Exception) {
|
||||
|
|
|
|||
|
|
@ -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<Message>()
|
||||
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String>()
|
||||
|
||||
// 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
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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/"
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String, List<FieldAlteration>>,
|
||||
): 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<String, List<FieldAlteration>>,
|
||||
) {
|
||||
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<FieldAlteration>?.sum() = this?.sumOf {
|
||||
expressionUseCase.computeExpression(
|
||||
sheet = sheet,
|
||||
alterations = alterations,
|
||||
expression = it.expression
|
||||
)
|
||||
} ?: 0
|
||||
}
|
||||
|
|
@ -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<Field>,
|
||||
) {
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.pixelized.shared.lwa.model.alteration
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
sealed interface AlterationJson
|
||||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<FieldJsonV1>,
|
||||
) : 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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -1,12 +1,17 @@
|
|||
package com.pixelized.shared.lwa.model.campaign
|
||||
|
||||
data class Campaign(
|
||||
val characters: Map<String, CharacterInstance>,
|
||||
val npcs: Map<String, CharacterInstance>,
|
||||
val characters: Map<CharacterInstance.Id, CharacterInstance>,
|
||||
val npcs: Map<CharacterInstance.Id, CharacterInstance>,
|
||||
) {
|
||||
data class CharacterInstance(
|
||||
val characteristic: Map<Characteristic, Int>,
|
||||
) {
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Skill>,
|
||||
val specialSkills: List<Skill>,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package com.pixelized.desktop.lwa.parser.dice
|
||||
package com.pixelized.shared.lwa.parser.dice
|
||||
|
||||
data class Dice(
|
||||
val modifier: Modifier?,
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.pixelized.desktop.lwa.parser.dice
|
||||
package com.pixelized.shared.lwa.parser.dice
|
||||
|
||||
class DiceParser {
|
||||
private val diceParser = Regex(
|
||||
|
|
@ -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)"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.pixelized.desktop.lwa.parser.word
|
||||
package com.pixelized.shared.lwa.parser.word
|
||||
|
||||
data class Word(
|
||||
val type: Type,
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.pixelized.desktop.lwa.parser.word
|
||||
package com.pixelized.shared.lwa.parser.word
|
||||
|
||||
class WordParser {
|
||||
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<String, List<FieldAlteration>>,
|
||||
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<String, List<FieldAlteration>>,
|
||||
expression: String,
|
||||
): Int {
|
||||
return expressionParser.parse(input = expression)?.let {
|
||||
computeExpression(
|
||||
sheet = sheet,
|
||||
alterations = alterations,
|
||||
expression = it,
|
||||
)
|
||||
} ?: 0
|
||||
}
|
||||
|
||||
fun computeExpression(
|
||||
sheet: CharacterSheet,
|
||||
alterations: Map<String, List<FieldAlteration>>,
|
||||
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<FieldAlteration>?.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<String, List<FieldAlteration>>,
|
||||
)
|
||||
|
||||
companion object {
|
||||
private const val MIN_OCCUPATION_VALUE = 40
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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 {
|
||||
Loading…
Add table
Add a link
Reference in a new issue