Link the player character ribbon to the server.
This commit is contained in:
parent
ed1b27039d
commit
4ed11660c3
17 changed files with 228 additions and 72 deletions
|
|
@ -20,6 +20,7 @@ import com.pixelized.desktop.lwa.repository.roll_history.RollHistoryRepository
|
||||||
import com.pixelized.desktop.lwa.repository.settings.SettingsFactory
|
import com.pixelized.desktop.lwa.repository.settings.SettingsFactory
|
||||||
import com.pixelized.desktop.lwa.repository.settings.SettingsRepository
|
import com.pixelized.desktop.lwa.repository.settings.SettingsRepository
|
||||||
import com.pixelized.desktop.lwa.repository.settings.SettingsStore
|
import com.pixelized.desktop.lwa.repository.settings.SettingsStore
|
||||||
|
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.CharacterDetailViewModel
|
||||||
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDiminishedViewModel
|
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDiminishedViewModel
|
||||||
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.PlayerRibbonFactory
|
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.PlayerRibbonFactory
|
||||||
|
|
@ -105,6 +106,7 @@ val factoryDependencies
|
||||||
factoryOf(::SettingsFactory)
|
factoryOf(::SettingsFactory)
|
||||||
factoryOf(::CampaignJsonFactory)
|
factoryOf(::CampaignJsonFactory)
|
||||||
factoryOf(::PlayerRibbonFactory)
|
factoryOf(::PlayerRibbonFactory)
|
||||||
|
factoryOf(::CharacterDetailFactory)
|
||||||
}
|
}
|
||||||
|
|
||||||
val viewModelDependencies
|
val viewModelDependencies
|
||||||
|
|
@ -134,4 +136,4 @@ val useCaseDependencies
|
||||||
factoryOf(::ExpressionUseCase)
|
factoryOf(::ExpressionUseCase)
|
||||||
factoryOf(::SettingsUseCase)
|
factoryOf(::SettingsUseCase)
|
||||||
factoryOf(::CharacterSheetUseCase)
|
factoryOf(::CharacterSheetUseCase)
|
||||||
}
|
}
|
||||||
|
|
@ -99,6 +99,10 @@ class ExpressionUseCase(
|
||||||
rollUseCase.roll(expression.dice)
|
rollUseCase.roll(expression.dice)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is Expression.UrlExpression -> {
|
||||||
|
0 // Ignore this case.
|
||||||
|
}
|
||||||
|
|
||||||
is Expression.WordExpression -> when (expression.word.type) {
|
is Expression.WordExpression -> when (expression.word.type) {
|
||||||
Word.Type.BDC -> evaluate(expressionParser.parse(sheet.damageBonus))
|
Word.Type.BDC -> evaluate(expressionParser.parse(sheet.damageBonus))
|
||||||
Word.Type.BDD -> evaluate(expressionParser.parse(sheet.damageBonus))
|
Word.Type.BDD -> evaluate(expressionParser.parse(sheet.damageBonus))
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,14 @@ sealed interface Expression {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class UrlExpression(
|
||||||
|
val url: String,
|
||||||
|
) : Expression {
|
||||||
|
override fun toString(): String {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data class DiceExpression(
|
data class DiceExpression(
|
||||||
val dice: Dice,
|
val dice: Dice,
|
||||||
) : Expression {
|
) : Expression {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,9 @@ package com.pixelized.desktop.lwa.parser.expression
|
||||||
|
|
||||||
import com.pixelized.desktop.lwa.parser.dice.DiceParser
|
import com.pixelized.desktop.lwa.parser.dice.DiceParser
|
||||||
import com.pixelized.desktop.lwa.parser.word.WordParser
|
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:
|
* Highly inspired by the following javascript implementation:
|
||||||
|
|
@ -15,6 +18,12 @@ class ExpressionParser(
|
||||||
private val tokenBreak = arrayOf(
|
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(
|
private val operators = mapOf(
|
||||||
'+' to Operator(
|
'+' to Operator(
|
||||||
evaluations = { first, second -> Expression.Add(first, second) },
|
evaluations = { first, second -> Expression.Add(first, second) },
|
||||||
|
|
@ -50,8 +59,11 @@ class ExpressionParser(
|
||||||
* Every characters that are not un the [Companion.tokenBreak] list can be part of a token.
|
* Every characters that are not un the [Companion.tokenBreak] list can be part of a token.
|
||||||
* @see Companion.tokenBreak
|
* @see Companion.tokenBreak
|
||||||
*/
|
*/
|
||||||
private fun isToken(): Boolean = stack.peek().let {
|
private fun isToken(
|
||||||
it != null && !tokenBreak.contains(it)
|
currentToken: String? = null,
|
||||||
|
): Boolean = stack.peek().let {
|
||||||
|
it != null && !tokenBreak.contains(it) ||
|
||||||
|
(tokenBreakException[it]?.any { currentToken?.contains(it) ?: false } ?: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -69,7 +81,7 @@ class ExpressionParser(
|
||||||
val token = StringBuilder()
|
val token = StringBuilder()
|
||||||
do {
|
do {
|
||||||
stack.pull().let(token::append)
|
stack.pull().let(token::append)
|
||||||
} while (isToken())
|
} while (isToken(token.toString()))
|
||||||
return token.toString()
|
return token.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -186,6 +198,16 @@ class ExpressionParser(
|
||||||
return Expression.DiceExpression(dice)
|
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)
|
throw Error.UnRecognizedToken(actual = token, expression = stack.input)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ class AlterationRepository(
|
||||||
private val activeAlterationIdMapFlow: HashMap<CharacterId, MutableStateFlow<List<AlterationId>>> =
|
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")))
|
hashMapOf("0f2117e9-e077-4354-8d77-20150df1c462" to MutableStateFlow(listOf("7c00dafa-a67d-4351-8ea9-67d933012cde", "65e37d32-3031-4bf8-9369-d2c45d2efac0")))
|
||||||
|
|
||||||
fun alterations(characterId: String): Flow<Map<String, List<FieldAlteration>>> {
|
fun alterationsFlow(characterId: String): Flow<Map<String, List<FieldAlteration>>> {
|
||||||
return activeAlterationIdMapFlow
|
return activeAlterationIdMapFlow
|
||||||
.getOrPut(characterId) { MutableStateFlow(emptyList()) }
|
.getOrPut(characterId) { MutableStateFlow(emptyList()) }
|
||||||
.map { activeAlterationIds ->
|
.map { activeAlterationIds ->
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class CharacterSheetRepository(
|
class CharacterSheetRepository(
|
||||||
private val store: CharacterSheetStore,
|
private val store: CharacterSheetStore,
|
||||||
|
|
@ -17,6 +16,7 @@ class CharacterSheetRepository(
|
||||||
private val scope = CoroutineScope(Dispatchers.IO + Job())
|
private val scope = CoroutineScope(Dispatchers.IO + Job())
|
||||||
|
|
||||||
val characterSheetPreviewFlow get() = store.previewFlow
|
val characterSheetPreviewFlow get() = store.previewFlow
|
||||||
|
val characterDetailFlow get() = store.detailFlow
|
||||||
|
|
||||||
fun characterPreview(characterId: String?): CharacterSheetPreview? {
|
fun characterPreview(characterId: String?): CharacterSheetPreview? {
|
||||||
return characterSheetPreviewFlow.value.firstOrNull { it.id == characterId }
|
return characterSheetPreviewFlow.value.firstOrNull { it.id == characterId }
|
||||||
|
|
@ -35,17 +35,8 @@ class CharacterSheetRepository(
|
||||||
|
|
||||||
fun characterDetailFlow(
|
fun characterDetailFlow(
|
||||||
characterId: String?,
|
characterId: String?,
|
||||||
forceUpdate: Boolean = false,
|
|
||||||
): StateFlow<CharacterSheet?> {
|
): StateFlow<CharacterSheet?> {
|
||||||
val initial = store.detailFlow.value[characterId]
|
val initial = store.detailFlow.value[characterId]
|
||||||
if (forceUpdate || initial == null) {
|
|
||||||
scope.launch {
|
|
||||||
characterDetail(
|
|
||||||
characterId = characterId,
|
|
||||||
forceUpdate = forceUpdate,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return store.detailFlow
|
return store.detailFlow
|
||||||
.map { sheets ->
|
.map { sheets ->
|
||||||
sheets[characterId]
|
sheets[characterId]
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ import org.jetbrains.compose.resources.painterResource
|
||||||
import org.koin.compose.viewmodel.koinViewModel
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
data class CharacterDetailUio(
|
data class CharacterDetailHeaderUio(
|
||||||
val id: String,
|
val id: String,
|
||||||
val portrait: String?,
|
val portrait: String?,
|
||||||
val name: String,
|
val name: String,
|
||||||
|
|
@ -55,7 +55,7 @@ data class CharacterDetailUio(
|
||||||
)
|
)
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
data class CharacterDynDetailUio(
|
data class CharacterDetailHeaderInstanceUio(
|
||||||
val hp: String,
|
val hp: String,
|
||||||
val pp: String,
|
val pp: String,
|
||||||
)
|
)
|
||||||
|
|
@ -109,8 +109,8 @@ fun CharacterDetail(
|
||||||
@Composable
|
@Composable
|
||||||
fun CharacterDetailContent(
|
fun CharacterDetailContent(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
character: CharacterDetailUio,
|
character: CharacterDetailHeaderUio,
|
||||||
dynDetail: State<CharacterDynDetailUio?>,
|
dynDetail: State<CharacterDetailHeaderInstanceUio?>,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
onDiminished: () -> Unit,
|
onDiminished: () -> Unit,
|
||||||
onHp: () -> Unit,
|
onHp: () -> Unit,
|
||||||
|
|
@ -139,7 +139,7 @@ fun CharacterDetailContent(
|
||||||
@Composable
|
@Composable
|
||||||
private fun Background(
|
private fun Background(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
character: CharacterDetailUio,
|
character: CharacterDetailHeaderUio,
|
||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
modifier = modifier.fillMaxSize(),
|
modifier = modifier.fillMaxSize(),
|
||||||
|
|
@ -168,8 +168,8 @@ private fun Background(
|
||||||
@Composable
|
@Composable
|
||||||
private fun CharacterHeader(
|
private fun CharacterHeader(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
character: CharacterDetailUio,
|
character: CharacterDetailHeaderUio,
|
||||||
dynDetail: State<CharacterDynDetailUio?>,
|
dynDetail: State<CharacterDetailHeaderInstanceUio?>,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
onDiminished: () -> Unit,
|
onDiminished: () -> Unit,
|
||||||
onHp: () -> Unit,
|
onHp: () -> Unit,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
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.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,
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun convertToCharacterDetailHeaderUio(
|
||||||
|
sheet: CharacterSheet?,
|
||||||
|
alterations: Map<String, List<FieldAlteration>>,
|
||||||
|
): CharacterDetailHeaderUio? {
|
||||||
|
if (sheet == null) return null
|
||||||
|
|
||||||
|
fun List<FieldAlteration>?.sum(): Int {
|
||||||
|
return this?.sumOf {
|
||||||
|
expressionUseCase.computeExpression(sheet = sheet, expression = it.expression)
|
||||||
|
} ?: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
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()}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,33 +13,43 @@ import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository
|
||||||
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
|
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
|
||||||
import com.pixelized.shared.lwa.model.campaign.damage
|
import com.pixelized.shared.lwa.model.campaign.damage
|
||||||
import com.pixelized.shared.lwa.model.campaign.power
|
import com.pixelized.shared.lwa.model.campaign.power
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.mapNotNull
|
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
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
|
|
||||||
class CharacterDetailViewModel(
|
class CharacterDetailViewModel(
|
||||||
private val characterRepository: CharacterSheetRepository,
|
private val characterRepository: CharacterSheetRepository,
|
||||||
private val campaignRepository: CampaignRepository,
|
private val campaignRepository: CampaignRepository,
|
||||||
private val alterationRepository: AlterationRepository,
|
private val alterationRepository: AlterationRepository,
|
||||||
|
private val characterDetailFactory: CharacterDetailFactory,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val displayedCharacterId = MutableStateFlow<String?>(null)
|
private val displayedCharacterId = MutableStateFlow<String?>(null)
|
||||||
|
|
||||||
val detail: StateFlow<CharacterDetailUio?> = displayedCharacterId.map { id ->
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
val sheet = characterRepository.characterDetail(id) ?: return@map null
|
val detail: StateFlow<CharacterDetailHeaderUio?> = displayedCharacterId.flatMapLatest { id ->
|
||||||
CharacterDetailUio(
|
if (id != null) {
|
||||||
id = sheet.id,
|
combine(
|
||||||
portrait = sheet.portrait,
|
characterRepository.characterDetailFlow(characterId = id),
|
||||||
name = sheet.name,
|
alterationRepository.alterationsFlow(characterId = id),
|
||||||
hp = "${sheet.hp}",
|
) { sheet, alteration ->
|
||||||
pp = "${sheet.pp}",
|
characterDetailFactory.convertToCharacterDetailHeaderUio(
|
||||||
mov = "${sheet.movement}"
|
sheet = sheet,
|
||||||
)
|
alterations = alteration,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
flowOf(null)
|
||||||
|
}
|
||||||
}.stateIn(
|
}.stateIn(
|
||||||
scope = viewModelScope,
|
scope = viewModelScope,
|
||||||
started = SharingStarted.Eagerly,
|
started = SharingStarted.Eagerly,
|
||||||
|
|
@ -48,15 +58,15 @@ class CharacterDetailViewModel(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@Stable
|
@Stable
|
||||||
fun collectDynamicDetailAsState(id: String): State<CharacterDynDetailUio?> {
|
fun collectDynamicDetailAsState(id: String): State<CharacterDetailHeaderInstanceUio?> {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val flow: StateFlow<CharacterDynDetailUio?> = remember(id) {
|
val flow: StateFlow<CharacterDetailHeaderInstanceUio?> = remember(id) {
|
||||||
combine(
|
combine(
|
||||||
characterRepository.characterDetailFlow(id),
|
characterRepository.characterDetailFlow(id),
|
||||||
campaignRepository.characterInstanceFlow(id = id),
|
campaignRepository.characterInstanceFlow(id = id),
|
||||||
) { sheet, instance ->
|
) { sheet, instance ->
|
||||||
if (sheet == null) return@combine null
|
if (sheet == null) return@combine null
|
||||||
CharacterDynDetailUio(
|
CharacterDetailHeaderInstanceUio(
|
||||||
hp = "${sheet.hp - instance.damage}",
|
hp = "${sheet.hp - instance.damage}",
|
||||||
pp = "${sheet.power - instance.power}",
|
pp = "${sheet.power - instance.power}",
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,48 @@
|
||||||
package com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon
|
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.campaign.Campaign
|
import com.pixelized.shared.lwa.model.campaign.Campaign
|
||||||
import com.pixelized.shared.lwa.model.campaign.damage
|
import com.pixelized.shared.lwa.model.campaign.damage
|
||||||
import com.pixelized.shared.lwa.model.campaign.power
|
import com.pixelized.shared.lwa.model.campaign.power
|
||||||
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet
|
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 {
|
class PlayerRibbonFactory(
|
||||||
|
private val expressionUseCase: ExpressionUseCase,
|
||||||
|
) {
|
||||||
|
|
||||||
fun convertToPlayerPortraitUio(
|
fun convertToPlayerPortraitUio(
|
||||||
characterSheet: CharacterSheet?,
|
characterSheet: CharacterSheet?,
|
||||||
characterInstance: Campaign.CharacterInstance,
|
characterInstance: Campaign.CharacterInstance,
|
||||||
|
alterations: Map<String, List<FieldAlteration>>,
|
||||||
): PlayerPortraitUio? {
|
): PlayerPortraitUio? {
|
||||||
if (characterSheet == null) return null
|
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()
|
||||||
|
|
||||||
return PlayerPortraitUio(
|
return PlayerPortraitUio(
|
||||||
id = characterSheet.id,
|
id = characterSheet.id,
|
||||||
portrait = characterSheet.thumbnail,
|
portrait = alterations[THUMBNAIL]
|
||||||
hp = characterSheet.hp - characterInstance.damage,
|
?.firstNotNullOfOrNull { (it.expression as? Expression.UrlExpression)?.url }
|
||||||
maxHp = characterSheet.hp,
|
?: characterSheet.thumbnail,
|
||||||
pp = characterSheet.pp - characterInstance.power,
|
hp = maxHp - characterInstance.damage,
|
||||||
maxPp = characterSheet.pp,
|
maxHp = maxHp,
|
||||||
|
pp = maxPp - characterInstance.power,
|
||||||
|
maxPp = maxPp,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -8,30 +8,49 @@ import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
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.campaign.CampaignRepository
|
||||||
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
|
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
|
||||||
import com.pixelized.desktop.lwa.repository.roll_history.RollHistoryRepository
|
import com.pixelized.desktop.lwa.repository.roll_history.RollHistoryRepository
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.flatMapMerge
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class PlayerRibbonViewModel(
|
class PlayerRibbonViewModel(
|
||||||
private val rollHistoryRepository: RollHistoryRepository,
|
private val rollHistoryRepository: RollHistoryRepository,
|
||||||
characterRepository: CharacterSheetRepository,
|
characterRepository: CharacterSheetRepository,
|
||||||
|
alterationRepository: AlterationRepository,
|
||||||
private val ribbonFactory: PlayerRibbonFactory,
|
private val ribbonFactory: PlayerRibbonFactory,
|
||||||
campaignRepository: CampaignRepository,
|
campaignRepository: CampaignRepository,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
val characters: StateFlow<List<PlayerPortraitUio>> = campaignRepository.campaignFlow
|
val characters: StateFlow<List<PlayerPortraitUio>> = campaignRepository.campaignFlow
|
||||||
.map { campaign ->
|
.flatMapMerge { campaign ->
|
||||||
campaign.characters.mapNotNull { entry ->
|
combine<PlayerPortraitUio?, List<PlayerPortraitUio>>(
|
||||||
ribbonFactory.convertToPlayerPortraitUio(
|
flows = campaign.characters.map { entry ->
|
||||||
characterSheet = characterRepository.characterDetail(characterId = entry.key),
|
combine(
|
||||||
characterInstance = entry.value,
|
characterRepository.characterDetailFlow(characterId = entry.key),
|
||||||
)
|
alterationRepository.alterationsFlow(characterId = entry.key),
|
||||||
}
|
) { sheet, alterations ->
|
||||||
}.stateIn(
|
ribbonFactory.convertToPlayerPortraitUio(
|
||||||
|
characterSheet = sheet,
|
||||||
|
characterInstance = entry.value,
|
||||||
|
alterations = alterations,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
transform = { headers ->
|
||||||
|
headers.mapNotNull { it }.toList()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.stateIn(
|
||||||
scope = viewModelScope,
|
scope = viewModelScope,
|
||||||
started = SharingStarted.Eagerly,
|
started = SharingStarted.Eagerly,
|
||||||
initialValue = emptyList()
|
initialValue = emptyList()
|
||||||
|
|
@ -39,6 +58,19 @@ class PlayerRibbonViewModel(
|
||||||
|
|
||||||
private val rolls = hashMapOf<String, MutableState<PlayerPortraitRollUio?>>()
|
private val rolls = hashMapOf<String, MutableState<PlayerPortraitRollUio?>>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
viewModelScope.launch {
|
||||||
|
campaignRepository.campaignFlow.collectLatest {
|
||||||
|
it.characters.keys.forEach { id ->
|
||||||
|
characterRepository.characterDetail(
|
||||||
|
characterId = id,
|
||||||
|
forceUpdate = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@Stable
|
@Stable
|
||||||
fun roll(characterId: String): State<PlayerPortraitRollUio?> {
|
fun roll(characterId: String): State<PlayerPortraitRollUio?> {
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,6 @@ class CharacterSheetFactory(
|
||||||
private val skillUseCase: ExpressionUseCase,
|
private val skillUseCase: ExpressionUseCase,
|
||||||
private val expressionUseCase: ExpressionUseCase,
|
private val expressionUseCase: ExpressionUseCase,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun convertToUio(
|
suspend fun convertToUio(
|
||||||
sheet: CharacterSheet?,
|
sheet: CharacterSheet?,
|
||||||
campaign: Campaign,
|
campaign: Campaign,
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ class CharacterSheetViewModel(
|
||||||
private val sheetFlow = combine(
|
private val sheetFlow = combine(
|
||||||
characterRepository.characterDetailFlow(characterId = argument.id),
|
characterRepository.characterDetailFlow(characterId = argument.id),
|
||||||
campaignRepository.campaignFlow,
|
campaignRepository.campaignFlow,
|
||||||
alteration.alterations(characterId = argument.id),
|
alteration.alterationsFlow(characterId = argument.id),
|
||||||
transform = { sheet, campaign, alterations ->
|
transform = { sheet, campaign, alterations ->
|
||||||
factory.convertToUio(
|
factory.convertToUio(
|
||||||
sheet = sheet,
|
sheet = sheet,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.pixelized.desktop.lwa.business
|
package com.pixelized.desktop.lwa.business
|
||||||
|
|
||||||
|
import com.pixelized.shared.lwa.usecase.CharacterSheetUseCase
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class DamageBonusUseCaseTest {
|
class DamageBonusUseCaseTest {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.pixelized.desktop.lwa.business
|
package com.pixelized.desktop.lwa.business
|
||||||
|
|
||||||
|
import com.pixelized.shared.lwa.usecase.CharacterSheetUseCase
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class SkillNormalizerUseCaseText {
|
class SkillNormalizerUseCaseText {
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,26 @@ class ExpressionParserTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun textUrlExpression() {
|
||||||
|
val parser = ExpressionParser(
|
||||||
|
diceParser = DiceParser(),
|
||||||
|
wordParser = WordParser(),
|
||||||
|
)
|
||||||
|
parser.test(
|
||||||
|
expression = "https://drive.google.com/uc?export=view&id=19ME-r7LiDQSrDCq8faVlTNQ7tfX8UUOO",
|
||||||
|
expected = Expression.UrlExpression("https://drive.google.com/uc?export=view&id=19ME-r7LiDQSrDCq8faVlTNQ7tfX8UUOO"),
|
||||||
|
)
|
||||||
|
parser.test(
|
||||||
|
expression = "www.google.fr",
|
||||||
|
expected = Expression.UrlExpression("www.google.fr"),
|
||||||
|
)
|
||||||
|
parser.test(
|
||||||
|
expression = "google.fr",
|
||||||
|
expected = Expression.UrlExpression("google.fr"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testFunctionExpression() {
|
fun testFunctionExpression() {
|
||||||
val parser = ExpressionParser(
|
val parser = ExpressionParser(
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,26 @@ data class CharacterSheet(
|
||||||
val roll: String,
|
val roll: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
object CharacteristicId {
|
||||||
|
const val PORTRAIT = "PORTRAIT"
|
||||||
|
const val THUMBNAIL = "THUMBNAIL"
|
||||||
|
const val LVL = "LEVEL"
|
||||||
|
const val STR = "STR"
|
||||||
|
const val DEX = "DEX"
|
||||||
|
const val CON = "CON"
|
||||||
|
const val HEI = "HEI"
|
||||||
|
const val INT = "INT"
|
||||||
|
const val POW = "POW"
|
||||||
|
const val CHA = "CHA"
|
||||||
|
const val MOV = "MOV"
|
||||||
|
const val HP = "HP"
|
||||||
|
const val PP = "PP"
|
||||||
|
const val DMG = "DMG"
|
||||||
|
const val ARMOR = "ARMOR"
|
||||||
|
const val LB = "LEARNING"
|
||||||
|
const val GHP = "HP_GROW"
|
||||||
|
}
|
||||||
|
|
||||||
object CommonSkillId {
|
object CommonSkillId {
|
||||||
const val COMBAT_ID = "COMBAT"
|
const val COMBAT_ID = "COMBAT"
|
||||||
const val DODGE_ID = "DODGE"
|
const val DODGE_ID = "DODGE"
|
||||||
|
|
@ -66,21 +86,4 @@ data class CharacterSheet(
|
||||||
const val SLEIGHT_OF_HAND_ID = "SLEIGHT_OF_HAND"
|
const val SLEIGHT_OF_HAND_ID = "SLEIGHT_OF_HAND"
|
||||||
const val AID_ID = "AID"
|
const val AID_ID = "AID"
|
||||||
}
|
}
|
||||||
|
|
||||||
object CharacteristicId {
|
|
||||||
const val STR = "STR"
|
|
||||||
const val DEX = "DEX"
|
|
||||||
const val CON = "CON"
|
|
||||||
const val HEI = "HEI"
|
|
||||||
const val INT = "INT"
|
|
||||||
const val POW = "POW"
|
|
||||||
const val CHA = "CHA"
|
|
||||||
const val MOV = "MOV"
|
|
||||||
const val HP = "HP"
|
|
||||||
const val PP = "PP"
|
|
||||||
const val DMG = "DMG"
|
|
||||||
const val ARMOR = "ARMOR"
|
|
||||||
const val LB = "LEARNING"
|
|
||||||
const val GHP = "HP_GROW"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue