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
|
||||
|
||||
|
|
@ -10,42 +10,42 @@ class DamageBonusUseCaseTest {
|
|||
val userCase = CharacterSheetUseCase()
|
||||
|
||||
(0 until 12).forEach {
|
||||
val result = userCase.defaultDamageBonus(sum = it)
|
||||
val result = userCase.damageBonus(sum = it)
|
||||
val expected = "-1d6"
|
||||
assert(result == expected) {
|
||||
"Expected:'$expected' bonus damage for stat:'$it' but was:'$result'"
|
||||
}
|
||||
}
|
||||
(12 until 18).forEach {
|
||||
val result = userCase.defaultDamageBonus(sum = it)
|
||||
val result = userCase.damageBonus(sum = it)
|
||||
val expected = "-1d4"
|
||||
assert(result == expected) {
|
||||
"Expected:'$expected' bonus damage for stat:'$it' but was:'$result'"
|
||||
}
|
||||
}
|
||||
(18 until 23).forEach {
|
||||
val result = userCase.defaultDamageBonus(sum = it)
|
||||
val result = userCase.damageBonus(sum = it)
|
||||
val expected = "+0"
|
||||
assert(result == expected) {
|
||||
"Expected:'$expected' bonus damage for stat:'$it' but was:'$result'"
|
||||
}
|
||||
}
|
||||
(23 until 30).forEach {
|
||||
val result = userCase.defaultDamageBonus(sum = it)
|
||||
val result = userCase.damageBonus(sum = it)
|
||||
val expected = "+1d4"
|
||||
assert(result == expected) {
|
||||
"Expected:'$expected' bonus damage for stat:'$it' but was:'$result'"
|
||||
}
|
||||
}
|
||||
(30 until 40).forEach {
|
||||
val result = userCase.defaultDamageBonus(sum = it)
|
||||
val result = userCase.damageBonus(sum = it)
|
||||
val expected = "+1d6"
|
||||
assert(result == expected) {
|
||||
"Expected:'$expected' bonus damage for stat:'$it' but was:'$result'"
|
||||
}
|
||||
}
|
||||
(40 until 100).forEach {
|
||||
val result = userCase.defaultDamageBonus(sum = it)
|
||||
val result = userCase.damageBonus(sum = it)
|
||||
val expected = "+2d6"
|
||||
assert(result == expected) {
|
||||
"Expected:'$expected' bonus damage for stat:'$it' but was:'$result'"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.pixelized.desktop.lwa.business
|
||||
|
||||
import com.pixelized.shared.lwa.usecase.RollUseCase
|
||||
import org.junit.Test
|
||||
|
||||
class RollUseCaseTest {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.pixelized.desktop.lwa.business
|
||||
|
||||
import com.pixelized.desktop.lwa.business.SkillStepUseCase.SkillStep
|
||||
import com.pixelized.shared.lwa.usecase.SkillStepUseCase
|
||||
import com.pixelized.shared.lwa.usecase.SkillStepUseCase.SkillStep
|
||||
import org.junit.Test
|
||||
|
||||
class SkillStepUseCaseTest {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package com.pixelized.desktop.lwa.parser.dice
|
||||
|
||||
import com.pixelized.shared.lwa.parser.dice.Dice
|
||||
import com.pixelized.shared.lwa.parser.dice.DiceParser
|
||||
import org.junit.Test
|
||||
|
||||
class DiceParserTest {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
package com.pixelized.desktop.lwa.parser.expression
|
||||
|
||||
import com.pixelized.desktop.lwa.parser.dice.DiceParser
|
||||
import com.pixelized.desktop.lwa.parser.expression.ExpressionParser.Error
|
||||
import com.pixelized.desktop.lwa.parser.word.WordParser
|
||||
import com.pixelized.shared.lwa.parser.dice.DiceParser
|
||||
import com.pixelized.shared.lwa.parser.expression.Expression
|
||||
import com.pixelized.shared.lwa.parser.expression.ExpressionParser
|
||||
import com.pixelized.shared.lwa.parser.expression.ExpressionParser.Error
|
||||
import com.pixelized.shared.lwa.parser.word.WordParser
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
|
|
@ -120,6 +122,20 @@ class ExpressionParserTest {
|
|||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testReadWrite() {
|
||||
val parser = ExpressionParser(
|
||||
diceParser = DiceParser(),
|
||||
wordParser = WordParser(),
|
||||
)
|
||||
parser.test(
|
||||
expression = "((1+2)*3)",
|
||||
)
|
||||
parser.test(
|
||||
expression = "(1+(2*3))",
|
||||
)
|
||||
}
|
||||
|
||||
private fun ExpressionParser.test(
|
||||
expression: String,
|
||||
expected: Expression?,
|
||||
|
|
@ -129,4 +145,13 @@ class ExpressionParserTest {
|
|||
"ExpressionParser.parse(input=$expression) is expected to return:$expected, but was:$result"
|
||||
}
|
||||
}
|
||||
|
||||
private fun ExpressionParser.test(
|
||||
expression: String,
|
||||
) {
|
||||
val result = parse(parse(expression)?.toString())?.toString()
|
||||
assert(result == expression) {
|
||||
"ExpressionParser.parse(input=$expression) is expected to return:$expression, but was:$result"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
package com.pixelized.desktop.lwa.parser.word
|
||||
|
||||
import com.pixelized.shared.lwa.parser.word.Word
|
||||
import com.pixelized.shared.lwa.parser.word.WordParser
|
||||
import org.junit.Test
|
||||
|
||||
class WordParserTest {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue