Add the alteration system to the server & link the app on it.

This commit is contained in:
Thomas Andres Gomez 2025-02-26 14:43:42 +01:00
parent 4ed11660c3
commit 29747dcb5c
83 changed files with 1797 additions and 811 deletions

View file

@ -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)
}

View file

@ -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
}
}

View file

@ -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)
}
}
}

View file

@ -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
}
}

View file

@ -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,
)
}

View file

@ -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>()
}

View file

@ -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}"
}
}

View file

@ -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
}
}
}

View file

@ -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"
}
}
}

View file

@ -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
}
}
}

View file

@ -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"
}
}

View file

@ -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
}
}
}

View file

@ -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,
)
}
}

View file

@ -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)
}
}
}

View file

@ -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,
)
}

View file

@ -1,6 +0,0 @@
package com.pixelized.desktop.lwa.repository.alteration.model
data class AlterationMetadata(
val name: String,
val description: String,
)

View file

@ -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,
)

View file

@ -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)
}
}

View file

@ -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,
)
}

View file

@ -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
}

View file

@ -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

View file

@ -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

View file

@ -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

View 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
}
},
),
)
}
}
}

View file

@ -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

View file

@ -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}",
)
}
}
}
}

View file

@ -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)

View file

@ -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(

View file

@ -94,7 +94,7 @@ fun CampaignScreen(
modifier = Modifier
.padding(all = 8.dp)
.fillMaxHeight(),
viewModel = characterDetailViewModel,
detailViewModel = characterDetailViewModel,
dismissedViewModel = dismissedViewModel,
)
},

View file

@ -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(

View file

@ -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}"
)
}
}

View file

@ -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
}

View file

@ -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,
// )
}
}

View file

@ -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

View file

@ -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)
},

View file

@ -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,
)
}
}

View file

@ -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 }
)

View file

@ -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,

View file

@ -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 = {

View file

@ -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
}

View file

@ -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,

View file

@ -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

View file

@ -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(

View file

@ -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(

View file

@ -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>

View file

@ -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,
)

View file

@ -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