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,77 +0,0 @@
 | 
			
		|||
package com.pixelized.desktop.lwa.business
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
import com.pixelized.desktop.lwa.parser.dice.Dice
 | 
			
		||||
import kotlin.math.abs
 | 
			
		||||
import kotlin.math.max
 | 
			
		||||
import kotlin.math.min
 | 
			
		||||
 | 
			
		||||
class RollUseCase {
 | 
			
		||||
 | 
			
		||||
    fun roll(dice: Dice): Int {
 | 
			
		||||
        return roll(
 | 
			
		||||
            modifier = dice.modifier,
 | 
			
		||||
            quantity = dice.quantity,
 | 
			
		||||
            faces = dice.faces,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun roll(
 | 
			
		||||
        modifier: Dice.Modifier? = null,
 | 
			
		||||
        quantity: Int = 1,
 | 
			
		||||
        faces: Int,
 | 
			
		||||
    ): Int {
 | 
			
		||||
        print("{")
 | 
			
		||||
        return sum(count = quantity) { left ->
 | 
			
		||||
            when (modifier) {
 | 
			
		||||
                Dice.Modifier.ADVANTAGE -> {
 | 
			
		||||
                    val roll1 = roll(faces = faces)
 | 
			
		||||
                    val roll2 = roll(faces = faces)
 | 
			
		||||
                    print("[$roll1,$roll2]")
 | 
			
		||||
                    max(roll1, roll2)
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                Dice.Modifier.DISADVANTAGE -> {
 | 
			
		||||
                    val roll1 = roll(faces = faces)
 | 
			
		||||
                    val roll2 = roll(faces = faces)
 | 
			
		||||
                    print("[$roll1,$roll2]")
 | 
			
		||||
                    min(roll1, roll2)
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                Dice.Modifier.EMPHASIS -> {
 | 
			
		||||
                    val roll1 = roll(faces = faces)
 | 
			
		||||
                    val roll2 = roll(faces = faces)
 | 
			
		||||
                    print("[$roll1,$roll2]")
 | 
			
		||||
                    val half = faces / 2
 | 
			
		||||
                    val roll1Abs = abs(half - roll1)
 | 
			
		||||
                    val roll2Abs = abs(half - roll2)
 | 
			
		||||
                    when {
 | 
			
		||||
                        roll1Abs == roll2Abs -> max(roll1, roll2)
 | 
			
		||||
                        roll1Abs < roll2Abs -> roll2
 | 
			
		||||
                        else -> roll1
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                null -> {
 | 
			
		||||
                    roll(faces = faces).also { print("$it") }
 | 
			
		||||
                }
 | 
			
		||||
            }.also {
 | 
			
		||||
                if (quantity > 1 && left != 1) print(",")
 | 
			
		||||
            }
 | 
			
		||||
        }.also {
 | 
			
		||||
            print("}")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun roll(faces: Int): Int {
 | 
			
		||||
        return (Math.random() * faces.toDouble() + 1.0).toInt()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun sum(count: Int, block: (Int) -> Int): Int {
 | 
			
		||||
        return if (count > 1) {
 | 
			
		||||
            block(count) + sum(count - 1, block)
 | 
			
		||||
        } else {
 | 
			
		||||
            block(count)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,71 +0,0 @@
 | 
			
		|||
package com.pixelized.desktop.lwa.business
 | 
			
		||||
 | 
			
		||||
import androidx.compose.ui.util.fastRoundToInt
 | 
			
		||||
import kotlin.math.max
 | 
			
		||||
import kotlin.math.min
 | 
			
		||||
import kotlin.math.round
 | 
			
		||||
 | 
			
		||||
class SkillStepUseCase {
 | 
			
		||||
 | 
			
		||||
    data class SkillStep(
 | 
			
		||||
        val criticalSuccess: IntRange,
 | 
			
		||||
        val specialSuccess: IntRange,
 | 
			
		||||
        val success: IntRange,
 | 
			
		||||
        val failure: IntRange,
 | 
			
		||||
        val criticalFailure: IntRange,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Helper method to compute the range in which a roll is a either critical, special, success or failure.
 | 
			
		||||
     */
 | 
			
		||||
    fun computeSkillStep(skill: Int): SkillStep {
 | 
			
		||||
        val criticalSuccess = 1..min(roundToInt { skill * 0.05f }, 99)
 | 
			
		||||
        val specialSuccess =
 | 
			
		||||
            (roundToInt { skill * 0.05f } + 1)..min(roundToInt { skill * 0.2f }, 99)
 | 
			
		||||
        val success = (roundToInt { skill * 0.2f } + 1)..min(skill, 99)
 | 
			
		||||
        val criticalFailure = 100 - max(4 - criticalSuccess.last, 0)..100
 | 
			
		||||
        val failure = (success.last + 1) until criticalFailure.first
 | 
			
		||||
 | 
			
		||||
        return SkillStep(
 | 
			
		||||
            criticalSuccess = criticalSuccess.takeIf { it.first <= it.last } ?: NONE,
 | 
			
		||||
            specialSuccess = specialSuccess.takeIf { it.first <= it.last } ?: NONE,
 | 
			
		||||
            success = success.takeIf { it.first <= it.last } ?: NONE,
 | 
			
		||||
            failure = failure.takeIf { it.first <= it.last } ?: NONE,
 | 
			
		||||
            criticalFailure = criticalFailure.takeIf { it.first <= it.last } ?: NONE,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private inline fun roundToInt(block: () -> Float): Int = round(block()).fastRoundToInt()
 | 
			
		||||
 | 
			
		||||
    fun exportWiki() {
 | 
			
		||||
        fun print(range: IntRange): String = when {
 | 
			
		||||
            range == NONE -> "-"
 | 
			
		||||
            range.first == range.last -> "${range.first}"
 | 
			
		||||
            else -> "${range.first} - ${range.last}"
 | 
			
		||||
        }
 | 
			
		||||
        repeat(100) { skill ->
 | 
			
		||||
            val step = computeSkillStep(skill + 1)
 | 
			
		||||
            println(
 | 
			
		||||
                "|!${skill + 1} " +
 | 
			
		||||
                        "|${print(step.criticalSuccess)} " +
 | 
			
		||||
                        "|${print(step.specialSuccess)} " +
 | 
			
		||||
                        "|${print(step.success)} " +
 | 
			
		||||
                        "|${print(step.failure)} " +
 | 
			
		||||
                        "|${print(step.criticalFailure)} " +
 | 
			
		||||
                        "|"
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun exportTest() {
 | 
			
		||||
        println("val expected = hashMapOf(")
 | 
			
		||||
        (1..500).forEach {
 | 
			
		||||
            println("    $it to ${computeSkillStep(it)},")
 | 
			
		||||
        }
 | 
			
		||||
        println(")")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        private val NONE: IntRange = -1..-1
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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,23 +0,0 @@
 | 
			
		|||
package com.pixelized.desktop.lwa.parser.dice
 | 
			
		||||
 | 
			
		||||
data class Dice(
 | 
			
		||||
    val modifier: Modifier?,
 | 
			
		||||
    val quantity: Int,
 | 
			
		||||
    val faces: Int,
 | 
			
		||||
) {
 | 
			
		||||
    enum class Modifier {
 | 
			
		||||
        ADVANTAGE,
 | 
			
		||||
        DISADVANTAGE,
 | 
			
		||||
        EMPHASIS,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun toString(): String {
 | 
			
		||||
        val modifier = when (modifier) {
 | 
			
		||||
            Modifier.ADVANTAGE -> "a"
 | 
			
		||||
            Modifier.DISADVANTAGE -> "d"
 | 
			
		||||
            Modifier.EMPHASIS -> "e"
 | 
			
		||||
            null -> ""
 | 
			
		||||
        }
 | 
			
		||||
        return "${modifier}${quantity}d${faces}"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,27 +0,0 @@
 | 
			
		|||
package com.pixelized.desktop.lwa.parser.dice
 | 
			
		||||
 | 
			
		||||
class DiceParser {
 | 
			
		||||
    private val diceParser = Regex(
 | 
			
		||||
        """^(?<modifier>[ade])?(?<quantity>\d+)[dD](?<faces>\d+)"""
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    fun parse(expression: String): Dice? {
 | 
			
		||||
        return diceParser.find(expression)?.let {
 | 
			
		||||
            val (modifier, quantity, faces) = it.destructured
 | 
			
		||||
            Dice(
 | 
			
		||||
                modifier = parseModifier(value = modifier),
 | 
			
		||||
                quantity = quantity.toInt(),
 | 
			
		||||
                faces = faces.toInt(),
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun parseModifier(value: String): Dice.Modifier? {
 | 
			
		||||
        return when (value) {
 | 
			
		||||
            "a" -> Dice.Modifier.ADVANTAGE
 | 
			
		||||
            "d" -> Dice.Modifier.DISADVANTAGE
 | 
			
		||||
            "e" -> Dice.Modifier.EMPHASIS
 | 
			
		||||
            else -> null
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,101 +0,0 @@
 | 
			
		|||
package com.pixelized.desktop.lwa.parser.expression
 | 
			
		||||
 | 
			
		||||
import com.pixelized.desktop.lwa.parser.dice.Dice
 | 
			
		||||
import com.pixelized.desktop.lwa.parser.word.Word
 | 
			
		||||
 | 
			
		||||
sealed interface Expression {
 | 
			
		||||
 | 
			
		||||
    data class Add(
 | 
			
		||||
        val first: Expression?,
 | 
			
		||||
        val second: Expression?,
 | 
			
		||||
    ) : Expression {
 | 
			
		||||
        override fun toString(): String {
 | 
			
		||||
            return "$first+$second"
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    data class Minus(
 | 
			
		||||
        val first: Expression?,
 | 
			
		||||
        val second: Expression?,
 | 
			
		||||
    ) : Expression {
 | 
			
		||||
        override fun toString(): String {
 | 
			
		||||
            return "$first-$second"
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    data class Div(
 | 
			
		||||
        val first: Expression?,
 | 
			
		||||
        val second: Expression?,
 | 
			
		||||
    ) : Expression {
 | 
			
		||||
        override fun toString(): String {
 | 
			
		||||
            return "$first/$second"
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    data class Prod(
 | 
			
		||||
        val first: Expression?,
 | 
			
		||||
        val second: Expression?,
 | 
			
		||||
    ) : Expression {
 | 
			
		||||
        override fun toString(): String {
 | 
			
		||||
            return "$first*$second"
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    data class Minimum(
 | 
			
		||||
        val first: Expression?,
 | 
			
		||||
        val second: Expression?,
 | 
			
		||||
    ) : Expression {
 | 
			
		||||
        override fun toString(): String {
 | 
			
		||||
            return "minimum($first,$second)"
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    data class Maximum(
 | 
			
		||||
        val first: Expression?,
 | 
			
		||||
        val second: Expression?,
 | 
			
		||||
    ) : Expression {
 | 
			
		||||
        override fun toString(): String {
 | 
			
		||||
            return "maximum($first,$second)"
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    data class Inversion(
 | 
			
		||||
        val expression: Expression,
 | 
			
		||||
    ) : Expression {
 | 
			
		||||
        override fun toString(): String {
 | 
			
		||||
            return "-($expression)"
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    data class UrlExpression(
 | 
			
		||||
        val url: String,
 | 
			
		||||
    ) : Expression {
 | 
			
		||||
        override fun toString(): String {
 | 
			
		||||
            return url
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    data class DiceExpression(
 | 
			
		||||
        val dice: Dice,
 | 
			
		||||
    ) : Expression {
 | 
			
		||||
        override fun toString(): String {
 | 
			
		||||
            return dice.toString()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    data class WordExpression(
 | 
			
		||||
        val word: Word,
 | 
			
		||||
    ) : Expression {
 | 
			
		||||
        override fun toString(): String {
 | 
			
		||||
            return word.toString()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    data class Flat(
 | 
			
		||||
        val value: Int,
 | 
			
		||||
    ) : Expression {
 | 
			
		||||
        override fun toString(): String {
 | 
			
		||||
            return "$value"
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,313 +0,0 @@
 | 
			
		|||
package com.pixelized.desktop.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 java.net.URI
 | 
			
		||||
import java.net.URL
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Highly inspired by the following javascript implementation:
 | 
			
		||||
 * https://tiarkrompf.github.io/notes/?/just-write-the-parser/
 | 
			
		||||
 */
 | 
			
		||||
class ExpressionParser(
 | 
			
		||||
    private val diceParser: DiceParser,
 | 
			
		||||
    private val wordParser: WordParser,
 | 
			
		||||
) {
 | 
			
		||||
    companion object {
 | 
			
		||||
        private val tokenBreak = arrayOf(
 | 
			
		||||
            '+', '-', '/', '*', '(', ')', ','
 | 
			
		||||
        )
 | 
			
		||||
        private val tokenBreakException = mapOf(
 | 
			
		||||
            '/' to listOf("http:", "https:"),
 | 
			
		||||
            '.' to listOf("http:", "https:"),
 | 
			
		||||
            '+' to listOf("http:", "https:"),
 | 
			
		||||
            '-' to listOf("http:", "https:"),
 | 
			
		||||
        )
 | 
			
		||||
        private val operators = mapOf(
 | 
			
		||||
            '+' to Operator(
 | 
			
		||||
                evaluations = { first, second -> Expression.Add(first, second) },
 | 
			
		||||
                priority = 100,
 | 
			
		||||
                association = 1,
 | 
			
		||||
            ),
 | 
			
		||||
            '-' to Operator(
 | 
			
		||||
                evaluations = { first, second -> Expression.Minus(first, second) },
 | 
			
		||||
                priority = 100,
 | 
			
		||||
                association = 1,
 | 
			
		||||
            ),
 | 
			
		||||
            '*' to Operator(
 | 
			
		||||
                evaluations = { first, second -> Expression.Prod(first, second) },
 | 
			
		||||
                priority = 200,
 | 
			
		||||
                association = 1,
 | 
			
		||||
            ),
 | 
			
		||||
            '/' to Operator(
 | 
			
		||||
                evaluations = { first, second -> Expression.Div(first, second) },
 | 
			
		||||
                priority = 200,
 | 
			
		||||
                association = 1,
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        private inline fun guard(condition: Boolean, error: () -> Error) {
 | 
			
		||||
            return if (condition.not()) throw error() else Unit
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private val stack = Stack()
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Helper method to determined when a token should end.
 | 
			
		||||
     * Every characters that are not un the [Companion.tokenBreak] list can be part of a token.
 | 
			
		||||
     * @see Companion.tokenBreak
 | 
			
		||||
     */
 | 
			
		||||
    private fun isToken(
 | 
			
		||||
        currentToken: String? = null,
 | 
			
		||||
    ): Boolean = stack.peek().let {
 | 
			
		||||
        it != null && !tokenBreak.contains(it) ||
 | 
			
		||||
                (tokenBreakException[it]?.any { currentToken?.contains(it) ?: false } ?: false)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Helper method to pull from the stack until we have a complete token.
 | 
			
		||||
     * Use the isToken() method to determined when a token should end.
 | 
			
		||||
     * @see isToken
 | 
			
		||||
     */
 | 
			
		||||
    private fun token(): String {
 | 
			
		||||
        guard(isToken()) {
 | 
			
		||||
            Error.ExpectedTokenCharacter(
 | 
			
		||||
                actual = stack.peek(),
 | 
			
		||||
                expression = stack.input,
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
        val token = StringBuilder()
 | 
			
		||||
        do {
 | 
			
		||||
            stack.pull().let(token::append)
 | 
			
		||||
        } while (isToken(token.toString()))
 | 
			
		||||
        return token.toString()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Helper method to construct an expression from a token.
 | 
			
		||||
     * This method handle bracket recursion, functions [min(), max(), etc.]
 | 
			
		||||
     * and token expression creation (Word, Dice, Flat, etc.).
 | 
			
		||||
     * @see token
 | 
			
		||||
     */
 | 
			
		||||
    private fun factor(): Expression? {
 | 
			
		||||
        when (stack.peek()) {
 | 
			
		||||
            '(' -> {
 | 
			
		||||
                stack.moveCursor()
 | 
			
		||||
                val result = evaluate()
 | 
			
		||||
                guard(stack.peek() == ')') {
 | 
			
		||||
                    Error.ExpectedOperator(
 | 
			
		||||
                        expected = ')',
 | 
			
		||||
                        actual = stack.peek(),
 | 
			
		||||
                        expression = stack.input
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
                stack.moveCursor()
 | 
			
		||||
                return result
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            '-' -> { // this is considered as a sign function for the following expression.
 | 
			
		||||
                stack.moveCursor()
 | 
			
		||||
                val result = evaluate()
 | 
			
		||||
                return result?.let(Expression::Inversion)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            '+' -> { // this is considered as a sign function for the following expression.
 | 
			
		||||
                stack.moveCursor()
 | 
			
		||||
                val result = evaluate()
 | 
			
		||||
                return result
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            else -> when (val token = token()) {
 | 
			
		||||
                "min" -> {
 | 
			
		||||
                    // consume the '(' character
 | 
			
		||||
                    stack.moveCursor()
 | 
			
		||||
                    // evaluate the content of the first parameter.
 | 
			
		||||
                    val first = evaluate()
 | 
			
		||||
                    // check that the expression is well formed, need a ,.
 | 
			
		||||
                    guard(stack.peek() == ',') {
 | 
			
		||||
                        Error.ExpectedOperator(
 | 
			
		||||
                            expected = ',',
 | 
			
		||||
                            actual = stack.peek(),
 | 
			
		||||
                            expression = stack.input
 | 
			
		||||
                        )
 | 
			
		||||
                    }
 | 
			
		||||
                    // consume the ',' character of the second parameter.
 | 
			
		||||
                    stack.moveCursor()
 | 
			
		||||
                    // evaluate the content of the second parameter.
 | 
			
		||||
                    val second = evaluate()
 | 
			
		||||
                    // check that the expression is well formed, need a ).
 | 
			
		||||
                    guard(stack.peek() == ')') {
 | 
			
		||||
                        Error.ExpectedOperator(
 | 
			
		||||
                            expected = ')',
 | 
			
		||||
                            actual = stack.peek(),
 | 
			
		||||
                            expression = stack.input
 | 
			
		||||
                        )
 | 
			
		||||
                    }
 | 
			
		||||
                    // consume the ')' character
 | 
			
		||||
                    stack.moveCursor()
 | 
			
		||||
                    // build the final function expression
 | 
			
		||||
                    return Expression.Minimum(first, second)
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                "max" -> {
 | 
			
		||||
                    // consume the '(' character
 | 
			
		||||
                    stack.moveCursor()
 | 
			
		||||
                    // evaluate the content of the first parameter.
 | 
			
		||||
                    val first = evaluate()
 | 
			
		||||
                    // check that the expression is well formed, need a ,.
 | 
			
		||||
                    guard(stack.peek() == ',') {
 | 
			
		||||
                        Error.ExpectedOperator(
 | 
			
		||||
                            expected = ',',
 | 
			
		||||
                            actual = stack.peek(),
 | 
			
		||||
                            expression = stack.input
 | 
			
		||||
                        )
 | 
			
		||||
                    }
 | 
			
		||||
                    // consume the ',' character of the second parameter.
 | 
			
		||||
                    stack.moveCursor()
 | 
			
		||||
                    // evaluate the content of the second parameter.
 | 
			
		||||
                    val second = evaluate()
 | 
			
		||||
                    // check that the expression is well formed, need a ).
 | 
			
		||||
                    guard(stack.peek() == ')') {
 | 
			
		||||
                        Error.ExpectedOperator(
 | 
			
		||||
                            expected = ')',
 | 
			
		||||
                            actual = stack.peek(),
 | 
			
		||||
                            expression = stack.input
 | 
			
		||||
                        )
 | 
			
		||||
                    }
 | 
			
		||||
                    // consume the ')' character
 | 
			
		||||
                    stack.moveCursor()
 | 
			
		||||
                    // build the final function expression
 | 
			
		||||
                    return Expression.Maximum(first, second)
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                else -> {
 | 
			
		||||
                    val value = token.toIntOrNull()
 | 
			
		||||
                    if (value != null) {
 | 
			
		||||
                        return Expression.Flat(value)
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    val word = wordParser.parse(token)
 | 
			
		||||
                    if (word != null) {
 | 
			
		||||
                        return Expression.WordExpression(word)
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    val dice = diceParser.parse(token)
 | 
			
		||||
                    if (dice != null) {
 | 
			
		||||
                        return Expression.DiceExpression(dice)
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    val url = try {
 | 
			
		||||
                        println(token)
 | 
			
		||||
                        URI.create(token).toString()
 | 
			
		||||
                    } catch (_: Exception) {
 | 
			
		||||
                        null
 | 
			
		||||
                    }
 | 
			
		||||
                    if (url != null) {
 | 
			
		||||
                        return Expression.UrlExpression(url)
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    throw Error.UnRecognizedToken(actual = token, expression = stack.input)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Helper method to handle operator priority and associativity.
 | 
			
		||||
     * build with the example https://tiarkrompf.github.io/notes/?/just-write-the-parser/ in mind.
 | 
			
		||||
     * @param minPriority the minimum priority to check.
 | 
			
		||||
     * @return a nullable [Expression]
 | 
			
		||||
     */
 | 
			
		||||
    private fun evaluate(minPriority: Int): Expression? {
 | 
			
		||||
        var res = factor()
 | 
			
		||||
        while (
 | 
			
		||||
            operators.contains(stack.peek()) &&
 | 
			
		||||
            (operators[stack.peek()]?.priority ?: 0) >= minPriority
 | 
			
		||||
        ) {
 | 
			
		||||
            val nextMin = operators[stack.peek()]?.let { it.priority + it.association } ?: 0
 | 
			
		||||
            res = operators[stack.pull()]?.evaluations?.invoke(
 | 
			
		||||
                res,
 | 
			
		||||
                evaluate(nextMin)
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
        return res
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Helper method to evaluate a expression that have been initialized into the stack.
 | 
			
		||||
     * This is typically call just after the stack have been initialized but also when a recursion is needed.
 | 
			
		||||
     * @return a nullable [Expression]
 | 
			
		||||
     */
 | 
			
		||||
    private fun evaluate(): Expression? {
 | 
			
		||||
        return evaluate(0)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Public parsing method for an expression.
 | 
			
		||||
     * @param input the input expression [String]
 | 
			
		||||
     * @return a nullable [Expression]
 | 
			
		||||
     */
 | 
			
		||||
    fun parse(input: String?): Expression? {
 | 
			
		||||
        return input.takeIf { it?.isNotBlank() == true }?.let {
 | 
			
		||||
            stack.init(expression = it)
 | 
			
		||||
            evaluate()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    sealed class Error(
 | 
			
		||||
        message: String,
 | 
			
		||||
        expression: String,
 | 
			
		||||
    ) : Exception("Expession:$expression - $message") {
 | 
			
		||||
 | 
			
		||||
        class ExpectedTokenCharacter(
 | 
			
		||||
            val actual: Char?,
 | 
			
		||||
            val expression: String,
 | 
			
		||||
        ) : Error(
 | 
			
		||||
            "Expected a token character (either a letter or a digit), but was: $actual", expression
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        class ExpectedOperator(
 | 
			
		||||
            val expected: Char,
 | 
			
		||||
            val actual: Char?,
 | 
			
		||||
            val expression: String,
 | 
			
		||||
        ) : Error("Expected operator: $expected, but was $actual", expression)
 | 
			
		||||
 | 
			
		||||
        class UnRecognizedToken(
 | 
			
		||||
            val actual: String,
 | 
			
		||||
            val expression: String,
 | 
			
		||||
        ) : Error(
 | 
			
		||||
            "Expected a specific token 'word', 'digit' or 'function', but was: $actual",
 | 
			
		||||
            expression
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private data class Operator(
 | 
			
		||||
        val evaluations: (Expression?, Expression?) -> Expression?,
 | 
			
		||||
        val priority: Int,      // operator priority, higher is priority
 | 
			
		||||
        val association: Int,   // 0 : left associativity, 1: right
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    private data class Stack(
 | 
			
		||||
        var input: String = "",
 | 
			
		||||
        private var cursor: Int = 0,
 | 
			
		||||
        private var peek: Char? = null,
 | 
			
		||||
    ) {
 | 
			
		||||
        fun init(expression: String) {
 | 
			
		||||
            input = expression
 | 
			
		||||
            cursor = 0
 | 
			
		||||
            peek = null
 | 
			
		||||
            moveCursor()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        fun peek(): Char? = peek
 | 
			
		||||
 | 
			
		||||
        fun pull(): Char? = peek().also { moveCursor() }
 | 
			
		||||
 | 
			
		||||
        fun moveCursor() {
 | 
			
		||||
            peek = input.getOrNull(cursor)
 | 
			
		||||
            cursor = (cursor + 1).takeIf { it < input.length } ?: -1
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,21 +0,0 @@
 | 
			
		|||
package com.pixelized.desktop.lwa.parser.word
 | 
			
		||||
 | 
			
		||||
data class Word(
 | 
			
		||||
    val type: Type,
 | 
			
		||||
) {
 | 
			
		||||
    enum class Type {
 | 
			
		||||
        BDC, // Damages bonus for melee
 | 
			
		||||
        BDD, // Damages bonus for range
 | 
			
		||||
        STR, // Strength
 | 
			
		||||
        DEX, // Dexterity
 | 
			
		||||
        CON, // Constitution
 | 
			
		||||
        HEI, // Height
 | 
			
		||||
        INT, // Intelligence
 | 
			
		||||
        POW, // Power
 | 
			
		||||
        CHA, // Charisma
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun toString(): String {
 | 
			
		||||
        return "$type"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,14 +0,0 @@
 | 
			
		|||
package com.pixelized.desktop.lwa.parser.word
 | 
			
		||||
 | 
			
		||||
class WordParser {
 | 
			
		||||
 | 
			
		||||
    fun parse(
 | 
			
		||||
        value: String,
 | 
			
		||||
    ): Word? {
 | 
			
		||||
        return try {
 | 
			
		||||
            Word(type = Word.Type.valueOf(value))
 | 
			
		||||
        } catch (_: Exception) {
 | 
			
		||||
            return null
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue