Add characterSheet and Campaing to the server.

This commit is contained in:
Thomas Andres Gomez 2025-02-22 21:25:08 +01:00
parent 1e5f0d88ae
commit 495768e5fe
53 changed files with 879 additions and 513 deletions

View file

@ -9,6 +9,8 @@ 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
import com.pixelized.desktop.lwa.repository.campaign.CampaignStore
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetStore
import com.pixelized.desktop.lwa.repository.network.NetworkRepository
@ -29,8 +31,7 @@ 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.shared.lwa.model.campaign.CampaignRepository
import com.pixelized.shared.lwa.model.campaign.model.CampaignFactory
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
@ -78,6 +79,7 @@ val storeDependencies
singleOf(::CharacterSheetStore)
singleOf(::SettingsStore)
singleOf(::AlterationStore)
singleOf(::CampaignStore)
}
val repositoryDependencies
@ -97,7 +99,7 @@ val factoryDependencies
factoryOf(::NetworkFactory)
factoryOf(::SkillFieldFactory)
factoryOf(::SettingsFactory)
factoryOf(::CampaignFactory)
factoryOf(::CampaignJsonFactory)
}
val viewModelDependencies

View file

@ -4,7 +4,7 @@ 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.model.CharacterSheet
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet
import kotlin.math.max
import kotlin.math.min
@ -32,9 +32,7 @@ class ExpressionUseCase(
val bonus = context.evaluate(
expression = skill.bonus?.let(expressionParser::parse),
)
val level = context.evaluate(
expression = skill.level?.let(expressionParser::parse),
)
val level = max((skill.level - 1) * 5, 0)
return max(base + bonus + level + alterations, 0)
}

View file

@ -4,24 +4,24 @@ 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.model.CharacterSheet.CharacteristicId
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CharacteristicId.ARMOR
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CharacteristicId.DEX
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CharacteristicId.HEI
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CharacteristicId.MOV
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CharacteristicId.STR
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CommonSkillId.ACROBATICS_ID
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CommonSkillId.AID_ID
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CommonSkillId.ATHLETICS_ID
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CommonSkillId.BARGAIN_ID
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CommonSkillId.COMBAT_ID
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CommonSkillId.DISCRETION_ID
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CommonSkillId.INTIMIDATION_ID
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CommonSkillId.PERCEPTION_ID
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CommonSkillId.PERSUASION_ID
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CommonSkillId.SLEIGHT_OF_HAND_ID
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CommonSkillId.SPIEL_ID
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CommonSkillId.THROW_ID
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
class AlterationStore(
private val expressionParser: ExpressionParser,

View file

@ -0,0 +1,40 @@
package com.pixelized.desktop.lwa.repository.campaign
import com.pixelized.shared.lwa.model.campaign.Campaign
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.stateIn
class CampaignRepository(
store: CampaignStore,
) {
private val scope = CoroutineScope(Dispatchers.IO + Job())
private val campaign = store.campaignFlow()
.stateIn(
scope = scope,
started = SharingStarted.Eagerly,
initialValue = Campaign.EMPTY,
)
fun campaignFlow(): StateFlow<Campaign> = campaign
fun characterInstance(id: String): StateFlow<Campaign.CharacterInstance> {
return campaign
.mapNotNull {
it.characters[id]
}
.stateIn(
scope = scope,
started = SharingStarted.Eagerly,
initialValue = campaign.value.characters[id] ?: Campaign.CharacterInstance(
characteristic = emptyMap(),
usedSkill = emptyList(),
)
)
}
}

View file

@ -0,0 +1,72 @@
package com.pixelized.desktop.lwa.repository.campaign
import com.pixelized.desktop.lwa.repository.network.NetworkRepository
import com.pixelized.shared.lwa.model.campaign.Campaign
import com.pixelized.shared.lwa.model.campaign.CampaignJson
import com.pixelized.shared.lwa.model.campaign.CampaignJsonFactory
import com.pixelized.shared.lwa.protocol.MessageType
import com.pixelized.shared.lwa.protocol.payload.UpdatePlayerCharacteristicMessage
import com.pixelized.shared.lwa.usecase.CampaignUseCase
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.request.get
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.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
class CampaignStore(
private val network: NetworkRepository,
private val factory: CampaignJsonFactory,
private val useCase: CampaignUseCase,
private val client: HttpClient,
private val json: Json,
) {
private val flow = MutableStateFlow(value = Campaign.EMPTY)
init {
val scope = CoroutineScope(Dispatchers.IO + Job())
scope.launch {
flow.value = load()
}
scope.launch {
network.data
.mapNotNull { it.takeIf { it.type == MessageType.UpdatePlayerCharacteristic } }
.map { json.decodeFromString<UpdatePlayerCharacteristicMessage>(it.value) }
.collect {
updateCharacteristic(it)
}
}
}
fun campaignFlow(): StateFlow<Campaign> = flow
private suspend fun load(): Campaign {
val request: CampaignJson = client
.get("http://pixelized.freeboxos.fr:16030/campaign") // TODO
.body()
val data = factory.convertFromJson(json = request)
return data
}
private fun updateCharacteristic(
message: UpdatePlayerCharacteristicMessage,
) {
val characters = flow.value.characters.toMutableMap()
val character = characters[message.characterId] ?: Campaign.CharacterInstance(
characteristic = emptyMap(),
usedSkill = emptyList(),
)
characters[message.characterId] = useCase.updateCharacteristic(
character = character,
characteristic = message.characteristic,
value = message.value
)
flow.value = flow.value.copy(characters = characters)
}
}

View file

@ -1,6 +1,6 @@
package com.pixelized.desktop.lwa.repository.characterSheet
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job

View file

@ -1,24 +1,31 @@
package com.pixelized.desktop.lwa.repository.characterSheet
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheetJson
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheetJsonFactory
import com.pixelized.desktop.lwa.repository.network.NetworkRepository
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJson
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJsonFactory
import com.pixelized.shared.lwa.protocol.MessageType
import com.pixelized.shared.lwa.protocol.payload.UpdateSkillUsageMessage
import com.pixelized.shared.lwa.usecase.CharacterSheetUseCase
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.request.get
import io.ktor.serialization.kotlinx.json.json
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.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
class CharacterSheetStore(
private val network: NetworkRepository,
private val factory: CharacterSheetJsonFactory,
private val useCase: CharacterSheetUseCase,
private val client: HttpClient,
private val json: Json,
) {
private val flow = MutableStateFlow<List<CharacterSheet>>(value = emptyList())
@ -27,21 +34,24 @@ class CharacterSheetStore(
scope.launch {
flow.value = load()
}
scope.launch {
network.data
.mapNotNull { it.takeIf { it.type == MessageType.UpdateSkillUsage } }
.map { json.decodeFromString<UpdateSkillUsageMessage>(it.value) }
.collect {
updateCharacterSkillChange(
characterId = it.characterId,
skillId = it.skillId,
)
}
}
}
fun characterSheetFlow(): StateFlow<List<CharacterSheet>> = flow
fun save(sheet: CharacterSheet) {
}
fun delete(id: String): Boolean {
return false
}
suspend fun load(): List<CharacterSheet> {
val request: List<CharacterSheetJson> = client
.get("http://pixelized.freeboxos.fr:16030/characters")
.get("http://pixelized.freeboxos.fr:16030/characters") // TODO
.body()
val data = request.map {
factory.convertFromJson(json = it)
@ -49,8 +59,19 @@ class CharacterSheetStore(
return data
}
sealed class CharacterSheetStoreException(root: Exception) : Exception(root)
class JsonConversionException(root: Exception) : CharacterSheetStoreException(root)
class FileWriteException(root: Exception) : CharacterSheetStoreException(root)
class FileReadException(root: Exception) : CharacterSheetStoreException(root)
private fun updateCharacterSkillChange(
characterId: String,
skillId: String,
) {
val characters = flow.value.toMutableList()
val index = characters.indexOfFirst { it.id == characterId }
if (index > -1) {
characters[index] = useCase.updateSkillUsage(
character = characters[index],
skillId = skillId,
)
flow.value = characters
}
}
}

View file

@ -7,6 +7,10 @@ import com.pixelized.desktop.lwa.utils.extention.encodeToFrame
import com.pixelized.shared.lwa.SERVER_PORT
import com.pixelized.shared.lwa.protocol.Message
import com.pixelized.shared.lwa.protocol.MessageType
import com.pixelized.shared.lwa.protocol.payload.MessagePayload
import com.pixelized.shared.lwa.protocol.payload.RollMessage
import com.pixelized.shared.lwa.protocol.payload.UpdatePlayerCharacteristicMessage
import com.pixelized.shared.lwa.protocol.payload.UpdateSkillUsageMessage
import io.ktor.client.HttpClient
import io.ktor.websocket.Frame
import kotlinx.coroutines.CoroutineScope
@ -19,11 +23,13 @@ import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
class NetworkRepository(
private val settingsRepository: SettingsRepository,
private val client: HttpClient,
private val json: Json,
) {
companion object {
const val DEFAULT_PORT = SERVER_PORT
@ -88,6 +94,33 @@ class NetworkRepository(
}
}
suspend fun share(
playerName: String = settingsRepository.settings().playerName,
payload: MessagePayload,
) {
if (status.value == Status.CONNECTED) {
when (payload) {
is RollMessage -> share(
playerName = playerName,
type = MessageType.Roll,
content = json.encodeToString(payload),
)
is UpdateSkillUsageMessage -> share(
playerName = playerName,
type = MessageType.UpdateSkillUsage,
content = json.encodeToString(payload),
)
is UpdatePlayerCharacteristicMessage -> share(
playerName = playerName,
type = MessageType.UpdatePlayerCharacteristic,
content = json.encodeToString(payload),
)
}
}
}
suspend fun share(
playerName: String = settingsRepository.settings().playerName,
type: MessageType,

View file

@ -2,7 +2,7 @@ package com.pixelized.desktop.lwa.repository.roll_history
import com.pixelized.desktop.lwa.repository.network.NetworkRepository
import com.pixelized.shared.lwa.protocol.MessageType
import com.pixelized.shared.lwa.protocol.roll.RollMessage
import com.pixelized.shared.lwa.protocol.payload.RollMessage
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharedFlow
@ -28,14 +28,6 @@ class RollHistoryRepository(
started = SharingStarted.Eagerly,
)
init {
scope.launch {
network.data.collect {
println(it)
}
}
}
suspend fun share(
characterId: String,
skillLabel: String,

View file

@ -51,13 +51,13 @@ data class CharacterDetailUio(
val name: String,
val hp: String,
val pp: String,
val mov: String,
)
@Stable
data class CharacterDynDetailUio(
val hp: String,
val pp: String,
val mov: String,
)
@Composable
@ -265,7 +265,7 @@ private fun CharacterHeader(
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.h6,
text = dynDetail.value.mov,
text = character.mov,
)
Text(
modifier = Modifier.alignByBaseline(),

View file

@ -8,7 +8,10 @@ import androidx.compose.runtime.remember
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.Campaign.CharacterInstance.Characteristic.Damage
import com.pixelized.shared.lwa.model.campaign.Campaign.CharacterInstance.Characteristic.Power
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
@ -17,24 +20,27 @@ import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.stateIn
class CharacterDetailViewModel(
private val repository: CharacterSheetRepository,
private val alteration: AlterationRepository,
private val characterRepository: CharacterSheetRepository,
private val campaignRepository: CampaignRepository,
private val alterationRepository: AlterationRepository,
) : ViewModel() {
private val displayedCharacterId = MutableStateFlow<String?>(null)
val detail: StateFlow<CharacterDetailUio?> = combine(
displayedCharacterId,
repository.characterSheetFlow(),
) { id, sheets ->
characterRepository.characterSheetFlow(),
campaignRepository.campaignFlow(),
) { id, sheets, campaign ->
val sheet = sheets.firstOrNull { it.id == id }
if (sheet == null) return@combine null
CharacterDetailUio(
id = sheet.id,
portrait = sheet.portrait,
name = sheet.name,
hp = "${sheet.maxHp}",
pp = "${sheet.maxPp}",
hp = "${sheet.hp - (campaign.characters[id]?.characteristic?.get(Damage) ?: 0)}",
pp = "${sheet.pp - (campaign.characters[id]?.characteristic?.get(Power) ?: 0)}",
mov = "${sheet.movement}"
)
}.stateIn(
scope = viewModelScope,
@ -46,22 +52,19 @@ class CharacterDetailViewModel(
@Stable
fun collectDynamicDetailAsState(id: String): State<CharacterDynDetailUio> {
val flow = remember(id) {
repository.characterSheetFlow(id = id)
campaignRepository.characterInstance(id = id)
}
return remember(id) {
flow.mapNotNull { sheet ->
if (sheet == null) return@mapNotNull null
CharacterDynDetailUio(
hp = sheet.currentHp.toString(),
pp = sheet.currentPp.toString(),
mov = sheet.movement.toString(),
hp = sheet.characteristic[Damage].toString(),
pp = sheet.characteristic[Power].toString(),
)
}
}.collectAsState(
initial = CharacterDynDetailUio(
hp = flow.value?.maxHp?.toString() ?: "",
pp = flow.value?.maxPp?.toString() ?: "",
mov = flow.value?.movement?.toString() ?: "",
CharacterDynDetailUio(
hp = flow.value.characteristic[Damage].toString(),
pp = flow.value.characteristic[Power].toString(),
)
)
}

View file

@ -35,9 +35,9 @@ import org.jetbrains.compose.resources.painterResource
data class PlayerPortraitUio(
val id: String,
val portrait: String?,
val hp: Int,
val damage: Int,
val maxHp: Int,
val pp: Int,
val usedPp: Int,
val maxPp: Int,
)
@ -96,7 +96,7 @@ fun PlayerPortrait(
Text(
modifier = Modifier.padding(bottom = 2.dp),
style = MaterialTheme.typography.caption,
text = "${character.hp}/${character.maxHp}",
text = "${character.maxHp - character.damage}/${character.maxHp}",
)
}
Row(
@ -111,7 +111,7 @@ fun PlayerPortrait(
Text(
modifier = Modifier.padding(bottom = 2.dp),
style = MaterialTheme.typography.caption,
text = "${character.pp}/${character.maxPp}",
text = "${character.maxPp - character.usedPp}/${character.maxPp}",
)
}
}

View file

@ -8,34 +8,42 @@ import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.desktop.lwa.repository.roll_history.RollHistoryRepository
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 kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
class PlayerRibbonViewModel(
private val rollHistoryRepository: RollHistoryRepository,
characterRepository: CharacterSheetRepository,
campaignRepository: CampaignRepository,
) : ViewModel() {
val characters: StateFlow<List<PlayerPortraitUio>> = characterRepository.characterSheetFlow()
.map { sheets ->
sheets.map { sheet ->
PlayerPortraitUio(
id = sheet.id,
portrait = sheet.thumbnail,
hp = sheet.currentHp,
maxHp = sheet.maxHp,
pp = sheet.currentPp,
maxPp = sheet.maxPp,
)
}
}.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = emptyList()
)
val characters: StateFlow<List<PlayerPortraitUio>> = combine(
characterRepository.characterSheetFlow(),
campaignRepository.campaignFlow(),
) { sheets, campaign ->
sheets.map { sheet ->
val instance = campaign.character(id = sheet.id)
PlayerPortraitUio(
id = sheet.id,
portrait = sheet.thumbnail,
damage = instance.damage,
maxHp = sheet.hp,
usedPp = instance.power,
maxPp = sheet.pp,
)
}
}.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = emptyList()
)
private val rolls = hashMapOf<String, MutableState<PlayerPortraitRollUio?>>()

View file

@ -1,12 +1,16 @@
package com.pixelized.desktop.lwa.ui.screen.characterSheet.detail
import com.pixelized.desktop.lwa.business.ExpressionUseCase
import com.pixelized.desktop.lwa.ui.composable.tooltip.TooltipUio
import com.pixelized.desktop.lwa.repository.alteration.model.FieldAlteration
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CharacteristicId
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.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 lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.character_sheet__characteristics__cha
import lwacharactersheet.composeapp.generated.resources.character_sheet__characteristics__con
@ -45,7 +49,7 @@ class CharacterSheetFactory(
suspend fun convertToUio(
sheet: CharacterSheet?,
diminished: Int,
campaign: Campaign,
alterations: Map<String, List<FieldAlteration>>,
): CharacterSheetPageUio? {
if (sheet == null) return null
@ -56,6 +60,10 @@ class CharacterSheetFactory(
} ?: 0
}
val maxHp = sheet.hp + alterations[CharacteristicId.HP].sum()
val maxPp = sheet.pp + alterations[CharacteristicId.PP].sum()
val instance = campaign.character(sheet.id)
return CharacterSheetPageUio(
id = sheet.id,
name = sheet.name,
@ -145,7 +153,7 @@ class CharacterSheetFactory(
Characteristic(
id = CharacteristicId.HP,
label = getString(Res.string.character_sheet__sub_characteristics__hit_point),
value = "${sheet.currentHp}/${sheet.maxHp + alterations[CharacteristicId.HP].sum()}",
value = "${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),
@ -155,7 +163,7 @@ class CharacterSheetFactory(
Characteristic(
id = CharacteristicId.PP,
label = getString(Res.string.character_sheet__sub_characteristics__power_point),
value = "${sheet.currentPp}/${sheet.maxPp + alterations[CharacteristicId.PP].sum()}",
value = "${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),
@ -205,6 +213,7 @@ class CharacterSheetFactory(
),
commonSkills = sheet.commonSkills.map { skill ->
Node(
id = skill.id,
label = skill.label,
value = skillUseCase.computeSkillValue(
sheet = sheet,
@ -222,6 +231,7 @@ class CharacterSheetFactory(
},
specialSKills = sheet.specialSkills.map { skill ->
Node(
id = skill.id,
label = skill.label,
tooltips = skill.description?.takeIf { it.isNotBlank() }?.let { description ->
TooltipUio(
@ -239,6 +249,7 @@ class CharacterSheetFactory(
},
magicsSkills = sheet.magicSkills.map { skill ->
Node(
id = skill.id,
label = skill.label,
tooltips = skill.description?.takeIf { it.isNotBlank() }?.let { description ->
TooltipUio(

View file

@ -110,6 +110,7 @@ data class CharacterSheetPageUio(
@Stable
data class Node(
val id: String,
val label: String,
val value: Int,
val tooltips: TooltipUio? = null,

View file

@ -9,27 +9,40 @@ import androidx.compose.ui.text.input.TextFieldValue
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.pixelized.desktop.lwa.ui.navigation.screen.destination.CharacterSheetDestination
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.characterSheet.model.CharacterSheet
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.desktop.lwa.utils.extention.collectAsState
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.payload.UpdatePlayerCharacteristicMessage
import com.pixelized.shared.lwa.protocol.payload.UpdateSkillUsageMessage
import kotlinx.coroutines.flow.SharingStarted
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__diminished__label
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
import kotlin.math.max
import kotlin.math.min
private typealias CSDCDialogUio = CharacterSheetDeleteConfirmationDialogUio
class CharacterSheetViewModel(
private val repository: CharacterSheetRepository,
private val characterRepository: CharacterSheetRepository,
private val campaignRepository: CampaignRepository,
private val network: NetworkRepository,
private val json: Json,
private val alteration: AlterationRepository,
private val factory: CharacterSheetFactory,
savedStateHandle: SavedStateHandle,
@ -49,7 +62,7 @@ class CharacterSheetViewModel(
private val _diminishedDialog = mutableStateOf<DiminishedStatDialogUio?>(null)
val diminishedDialog: State<DiminishedStatDialogUio?> get() = _diminishedDialog
private val diminishedValueFlow = repository.characterDiminishedFlow(id = argument.id)
private val diminishedValueFlow = characterRepository.characterDiminishedFlow(id = argument.id)
val diminishedValue: State<Int?>
@Composable
get() = diminishedValueFlow.collectAsState { it ->
@ -57,53 +70,46 @@ class CharacterSheetViewModel(
}
private val sheetFlow = combine(
repository.characterSheetFlow(id = argument.id),
repository.characterDiminishedFlow(id = argument.id),
characterRepository.characterSheetFlow(id = argument.id),
campaignRepository.campaignFlow(),
alteration.alterations(characterId = argument.id),
transform = { sheet, diminished, alterations ->
factory.convertToUio(sheet = sheet, diminished = diminished, alterations = alterations)
transform = { sheet, campaign, alterations ->
factory.convertToUio(
sheet = sheet,
campaign = campaign,
alterations = alterations
)
},
).stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = null,
)
val sheet: State<CharacterSheetPageUio?>
@Composable
get() = sheetFlow.collectAsState(
initial = null,
context = viewModelScope.coroutineContext,
)
get() = sheetFlow.collectAsState()
fun toggleWolf() {
alteration.toggle(argument.id, "65e37d32-3031-4bf8-9369-d2c45d2efac0")
}
fun deleteCharacter(id: String) {
repository.delete(id = id)
characterRepository.delete(id = id)
}
fun onUseSkill(skill: CharacterSheetPageUio.Node) {
repository.characterSheetFlow(id = argument.id).value?.let { sheet ->
val skills = sheet.commonSkills.map {
if (it.label == skill.label) it.copy(used = it.used.not()) else it
}
val occupations = sheet.specialSkills.map {
if (it.label == skill.label) it.copy(used = it.used.not()) else it
}
val magics = sheet.magicSkills.map {
if (it.label == skill.label) it.copy(used = it.used.not()) else it
}
repository.save(
characterSheet = sheet.copy(
commonSkills = skills,
specialSkills = occupations,
magicSkills = magics,
viewModelScope.launch {
network.share(
payload = UpdateSkillUsageMessage(
characterId = argument.id,
skillId = skill.id,
)
)
}
}
fun showConfirmCharacterDeletionDialog() {
repository.characterSheetFlow(id = argument.id).value?.let { sheet ->
characterRepository.characterSheetFlow(id = argument.id).value?.let { sheet ->
_displayDeleteConfirmationDialog.value = CharacterSheetDeleteConfirmationDialogUio(
id = sheet.id,
name = sheet.name,
@ -116,35 +122,36 @@ class CharacterSheetViewModel(
}
suspend fun showSubCharacteristicDialog(id: String) {
repository.characterSheetFlow(id = argument.id).value?.let { sheet ->
characterRepository.characterSheetFlow(id = argument.id).value?.let { sheet ->
val instance = campaignRepository.characterInstance(id = argument.id).value
_statChangeDialog.value = when (id) {
CharacterSheet.CharacteristicId.HP -> {
val value = mutableStateOf(
"${sheet.currentHp}".let {
"${sheet.hp - instance.damage}".let {
TextFieldValue(text = it, selection = TextRange(it.length))
}
)
StatChangeDialogUio(
id = CharacterSheet.CharacteristicId.HP,
id = Characteristic.Damage,
label = getString(resource = Res.string.character_sheet_edit__sub_characteristics__hit_point),
value = { value.value },
onValueChange = { value.value = it },
maxValue = "${sheet.maxHp}",
maxValue = "${sheet.hp}",
)
}
CharacterSheet.CharacteristicId.PP -> {
val value = mutableStateOf(
"${sheet.currentPp}".let {
"${sheet.power - instance.power}".let {
TextFieldValue(text = it, selection = TextRange(it.length))
}
)
StatChangeDialogUio(
id = CharacterSheet.CharacteristicId.PP,
id = Characteristic.Power,
label = getString(resource = Res.string.character_sheet_edit__sub_characteristics__power_point),
value = { value.value },
onValueChange = { value.value = it },
maxValue = "${sheet.maxPp}",
maxValue = "${sheet.power}",
)
}
@ -158,29 +165,24 @@ class CharacterSheetViewModel(
}
fun changeSubCharacteristic(
characteristicId: String,
characteristicId: Characteristic,
value: Int,
) {
val sheet = repository.characterSheetFlow(id = argument.id).value
val updated = when (characteristicId) {
CharacterSheet.CharacteristicId.HP -> sheet?.copy(
currentHp = max(
0,
min(sheet.maxHp, value)
viewModelScope.launch {
val sheet = characterRepository.characterSheetFlow(id = argument.id).value
if (sheet != null) {
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
},
),
)
)
CharacterSheet.CharacteristicId.PP -> sheet?.copy(
currentPp = max(
0,
min(sheet.maxPp, value)
)
)
else -> null
}
updated?.let {
repository.save(it)
}
}
}
@ -193,7 +195,7 @@ class CharacterSheetViewModel(
}
suspend fun showDiminishedDialog() {
val diminished = repository.characterDiminishedFlow(id = argument.id).value
val diminished = characterRepository.characterDiminishedFlow(id = argument.id).value
val textFieldValue =
mutableStateOf(TextFieldValue("$diminished", selection = TextRange(index = 0)))
_diminishedDialog.value = DiminishedStatDialogUio(
@ -215,7 +217,7 @@ class CharacterSheetViewModel(
fun changeDiminished(dialog: DiminishedStatDialogUio) {
val value = dialog.value().text.toIntOrNull() ?: 0
repository.setDiminishedForCharacter(
characterRepository.setDiminishedForCharacter(
id = dialog.id,
diminished = value,
)

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 StatChangeDialogUio(
val id: String,
val id: Campaign.CharacterInstance.Characteristic,
val label: String,
val value: () -> TextFieldValue,
val onValueChange: (TextFieldValue) -> Unit,

View file

@ -3,7 +3,7 @@ package com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.preview
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CharacteristicId
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId
import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.CharacterSheetPageUio
import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.CharacterSheetPageUio.Characteristic

View file

@ -8,7 +8,7 @@ import com.pixelized.desktop.lwa.ui.screen.characterSheet.edit.common.occupation
import com.pixelized.desktop.lwa.ui.screen.characterSheet.edit.composable.ActionFieldUio
import com.pixelized.desktop.lwa.ui.screen.characterSheet.edit.composable.BaseSkillFieldUio
import com.pixelized.desktop.lwa.ui.screen.characterSheet.edit.composable.SimpleFieldUio
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet
import com.pixelized.shared.lwa.usecase.CharacterSheetUseCase
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__actions__action_label
@ -41,13 +41,6 @@ import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__sk
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__special_title
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__spiel
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__throw
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__sub_characteristics__armor
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__sub_characteristics__damage_bonus
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__sub_characteristics__hp_grow
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__sub_characteristics__learning
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__sub_characteristics__max_hit_point
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__sub_characteristics__max_power_point
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__sub_characteristics__movement
import org.jetbrains.compose.resources.getString
import java.util.UUID
import kotlin.math.max
@ -60,56 +53,57 @@ class CharacterSheetEditFactory(
currentSheet: CharacterSheet?,
editedSheet: CharacterSheetEditPageUio,
): CharacterSheet {
val editedMaxHp = editedSheet.maxHp.unpack()?.toIntOrNull() ?: currentSheet?.maxHp ?: 0
val editedMaxPp = editedSheet.maxPp.unpack()?.toIntOrNull() ?: currentSheet?.maxPp ?: 0
val level = currentSheet?.level ?: 1
val strength = editedSheet.strength.unpack()?.toIntOrNull()
?: currentSheet?.strength
?: 0
val dexterity = editedSheet.dexterity.unpack()?.toIntOrNull()
?: currentSheet?.dexterity
?: 0
val constitution = editedSheet.constitution.unpack()?.toIntOrNull()
?: currentSheet?.constitution
?: 0
val height = editedSheet.height.unpack()?.toIntOrNull()
?: currentSheet?.height
?: 0
val intelligence = editedSheet.intelligence.unpack()?.toIntOrNull()
?: currentSheet?.intelligence
?: 0
val power = editedSheet.power.unpack()?.toIntOrNull()
?: currentSheet?.power
?: 0
val charisma = editedSheet.charisma.unpack()?.toIntOrNull()
?: currentSheet?.charisma
?: 0
return CharacterSheet(
id = editedSheet.id,
name = editedSheet.name.value.value,
portrait = currentSheet?.portrait,
thumbnail = currentSheet?.thumbnail,
strength = editedSheet.strength.unpack()?.toIntOrNull()
?: currentSheet?.strength
?: 0,
dexterity = editedSheet.dexterity.unpack()?.toIntOrNull()
?: currentSheet?.dexterity
?: 0,
constitution = editedSheet.constitution.unpack()?.toIntOrNull()
?: currentSheet?.constitution
?: 0,
height = editedSheet.height.unpack()?.toIntOrNull()
?: currentSheet?.height
?: 0,
intelligence = editedSheet.intelligence.unpack()?.toIntOrNull()
?: currentSheet?.intelligence
?: 0,
power = editedSheet.power.unpack()?.toIntOrNull()
?: currentSheet?.power
?: 0,
charisma = editedSheet.charisma.unpack()?.toIntOrNull()
?: currentSheet?.charisma
?: 0,
overrideMovement = editedSheet.movement.value.value.value.isNotBlank(),
movement = editedSheet.movement.unpack()?.toIntOrNull()
?: currentSheet?.movement
?: 10,
overrideMaxHp = editedSheet.maxHp.value.value.value.isNotBlank(),
maxHp = editedMaxHp,
currentHp = currentSheet?.currentHp?.coerceAtMost(editedMaxHp) ?: editedMaxHp,
overrideMaxPP = editedSheet.maxPp.value.value.value.isNotBlank(),
maxPp = editedMaxPp,
currentPp = currentSheet?.currentPp?.coerceAtMost(editedMaxPp) ?: editedMaxPp,
overrideDamageBonus = editedSheet.damageBonus.value.value.value.isNotBlank(),
damageBonus = editedSheet.damageBonus.unpack()
?: currentSheet?.damageBonus
?: "",
overrideArmor = editedSheet.armor.value.value.value.isNotBlank(),
armor = editedSheet.armor.unpack()?.toIntOrNull()
?: currentSheet?.armor
?: 0,
overrideLearning = editedSheet.learning.value.value.value.isNotBlank(),
learning = editedSheet.learning.unpack()?.toIntOrNull() ?: 0,
overrideHpGrow = editedSheet.hpGrow.value.value.value.isNotBlank(),
hpGrow = editedSheet.hpGrow.unpack()?.toIntOrNull() ?: 0,
level = level,
strength = strength,
dexterity = dexterity,
constitution = constitution,
height = height,
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),
commonSkills = editedSheet.commonSkills.map { editedSkill ->
val currentSkill = currentSheet?.commonSkills?.firstOrNull {
it.id == editedSkill.id
@ -120,7 +114,7 @@ class CharacterSheetEditFactory(
description = currentSkill?.description,
base = "${editedSkill.base.value}",
bonus = editedSkill.bonus.value.value.takeIf { it.isNotBlank() },
level = editedSkill.level.value.value.takeIf { it.isNotBlank() },
level = editedSkill.level.value.value.toIntOrNull() ?: 0,
occupation = editedSkill.option.checked.value,
used = currentSkill?.used ?: false,
)
@ -135,7 +129,7 @@ class CharacterSheetEditFactory(
description = editedSkill.description.value.value,
base = editedSkill.base.value.value,
bonus = editedSkill.bonus.value.value.takeIf { it.isNotBlank() },
level = editedSkill.level.value.value.takeIf { it.isNotBlank() },
level = editedSkill.level.value.value.toIntOrNull() ?: 0,
occupation = editedSkill.options.occupation,
used = currentSkill?.used ?: false,
)
@ -150,7 +144,7 @@ class CharacterSheetEditFactory(
description = editedSkill.description.value.value,
base = editedSkill.base.value.value,
bonus = editedSkill.bonus.value.value.takeIf { it.isNotBlank() },
level = editedSkill.level.value.value.takeIf { it.isNotBlank() },
level = editedSkill.level.value.value.toIntOrNull() ?: 0,
occupation = editedSkill.options.occupation,
used = currentSkill?.used ?: false,
)
@ -159,6 +153,8 @@ class CharacterSheetEditFactory(
CharacterSheet.Roll(
id = it.id,
label = it.label.value.value,
description = null, // TODO
canBeCritical = false, // TODO
roll = it.action.value.value,
)
},
@ -244,77 +240,6 @@ class CharacterSheetEditFactory(
intelligence = int,
power = pow,
charisma = cha,
movement = SimpleFieldUio(
label = getString(Res.string.character_sheet_edit__sub_characteristics__movement),
value = skillFieldFactory.createWrapper(
value = if (sheet?.overrideMovement == true) "${sheet.movement}" else "",
placeholder = derivedStateOf {
"${characterSheetUseCase.defaultMovement()}"
},
)
),
maxHp = SimpleFieldUio(
label = getString(Res.string.character_sheet_edit__sub_characteristics__max_hit_point),
value = skillFieldFactory.createWrapper(
value = if (sheet?.overrideMaxHp == true) "${sheet.maxHp}" else "",
placeholder = derivedStateOf {
"${
characterSheetUseCase.defaultMaxHp(
constitution = con(),
height = hei()
)
}"
},
)
),
maxPp = SimpleFieldUio(
label = getString(Res.string.character_sheet_edit__sub_characteristics__max_power_point),
value = skillFieldFactory.createWrapper(
value = if (sheet?.overrideMaxPP == true) "${sheet.maxPp}" else "",
placeholder = derivedStateOf {
"${characterSheetUseCase.defaultMaxPower(power = pow())}"
},
)
),
damageBonus = SimpleFieldUio(
label = getString(Res.string.character_sheet_edit__sub_characteristics__damage_bonus),
value = skillFieldFactory.createWrapper(
value = if (sheet?.overrideDamageBonus == true) sheet.damageBonus else "",
placeholder = derivedStateOf {
characterSheetUseCase.defaultDamageBonus(
strength = str(),
height = hei()
)
},
)
),
armor = SimpleFieldUio(
label = getString(Res.string.character_sheet_edit__sub_characteristics__armor),
value = skillFieldFactory.createWrapper(
value = if (sheet?.overrideArmor == true) "${sheet.armor}" else "",
placeholder = derivedStateOf {
"${characterSheetUseCase.defaultArmor()}"
},
)
),
learning = SimpleFieldUio(
label = getString(Res.string.character_sheet_edit__sub_characteristics__learning),
value = skillFieldFactory.createWrapper(
value = if (sheet?.overrideLearning == true) "${sheet.learning}" else "",
placeholder = derivedStateOf {
"${characterSheetUseCase.defaultLearning(intelligence = int())}"
},
)
),
hpGrow = SimpleFieldUio(
label = getString(Res.string.character_sheet_edit__sub_characteristics__hp_grow),
value = skillFieldFactory.createWrapper(
value = if (sheet?.overrideHpGrow == true) "${sheet.hpGrow}" else "",
placeholder = derivedStateOf {
"${characterSheetUseCase.defaultHpGrow(constitution = con())}"
},
)
),
commonSkills = listOf(
createBaseSkill(
sheet = sheet,
@ -421,7 +346,7 @@ class CharacterSheetEditFactory(
labelValue = skill.label,
baseValue = skill.base,
bonusValue = skill.bonus ?: "",
levelValue = skill.level ?: "",
levelValue = skill.level.takeIf { it > 0 }?.toString() ?: "",
options = run {
val current = sheet.specialSkills.firstOrNull { it.id == skill.id }
listOf(
@ -439,7 +364,7 @@ class CharacterSheetEditFactory(
labelValue = skill.label,
baseValue = skill.base,
bonusValue = skill.bonus ?: "",
levelValue = skill.level ?: "",
levelValue = skill.level.takeIf { it > 0 }?.toString() ?: "",
options = run {
val current = sheet.magicSkills.firstOrNull { it.id == skill.id }
listOf(
@ -487,7 +412,7 @@ class CharacterSheetEditFactory(
),
level = skillFieldFactory.createWrapper(
label = mutableStateOf(getString(Res.string.character_sheet_edit__skills__level_label)),
value = skill?.level ?: "",
value = skill?.level?.takeIf { it > 0 }?.toString() ?: "",
),
option = skillFieldFactory.occupationOption(skill?.occupation ?: false),
)

View file

@ -67,13 +67,6 @@ data class CharacterSheetEditPageUio(
val intelligence: SimpleFieldUio,
val power: SimpleFieldUio,
val charisma: SimpleFieldUio,
val movement: SimpleFieldUio,
val maxHp: SimpleFieldUio,
val maxPp: SimpleFieldUio,
val damageBonus: SimpleFieldUio,
val armor: SimpleFieldUio,
val learning: SimpleFieldUio,
val hpGrow: SimpleFieldUio,
val commonSkills: List<BaseSkillFieldUio>,
val specialSkills: List<SkillFieldUio>,
val magicSkills: List<SkillFieldUio>,
@ -89,17 +82,6 @@ data class CharacterSheetEditPageUio(
power,
charisma,
)
val subCharacteristics
get() = listOf(
movement,
maxHp,
maxPp,
damageBonus,
armor,
learning,
hpGrow,
)
}
@Composable
@ -215,27 +197,6 @@ fun CharacterSheetEdit(
}
}
}
DecoratedBox(
modifier = Modifier.weight(weight = 1f),
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
modifier = Modifier.padding(vertical = 8.dp),
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = MaterialTheme.typography.caption,
text = stringResource(Res.string.character_sheet_edit__sub_characteristics__title),
)
form.subCharacteristics.forEach {
SimpleField(
modifier = Modifier.fillMaxWidth(),
field = it,
)
}
}
}
}
DecoratedBox(

View file

@ -12,7 +12,7 @@ import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetReposit
import com.pixelized.desktop.lwa.repository.roll_history.RollHistoryRepository
import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.CharacterSheetPageUio
import com.pixelized.desktop.lwa.ui.screen.roll.DifficultyUio.Difficulty
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet
import kotlinx.coroutines.Job
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay

View file

@ -6,7 +6,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.desktop.lwa.repository.roll_history.RollHistoryRepository
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch

View file

@ -10,6 +10,7 @@ koin = "4.0.0"
turtle = "0.5.0"
logback = "1.5.11"
coil = "3.1.0"
filament-android = "1.17.1"
[plugins]
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }
@ -50,4 +51,5 @@ turtle = { group = "com.lordcodes.turtle", name = "turtle", version.ref = "turtl
logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }
coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" }
coil-network = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" }
coil-network = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" }
filament-android = { group = "com.google.ar.sceneform", name = "filament-android", version.ref = "filament-android" }

View file

@ -22,4 +22,5 @@ dependencies {
implementation(libs.ktor.server.websockets)
implementation(libs.ktor.server.negotiation)
implementation(libs.ktor.serialization.json)
implementation(libs.filament.android)
}

View file

@ -1,5 +1,8 @@
import com.pixelized.server.lwa.model.character.CharacterSheetRepository
import com.pixelized.server.lwa.model.campaign.CampaignService
import com.pixelized.server.lwa.model.campaign.CampaignStore
import com.pixelized.server.lwa.model.character.CharacterSheetService
import com.pixelized.server.lwa.model.character.CharacterSheetStore
import com.pixelized.server.lwa.server.Engine
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
@ -9,17 +12,25 @@ val serverModuleDependencies
factoryDependencies,
useCaseDependencies,
storeDependencies,
repositoryDependencies,
serviceDependencies,
engineDependencies,
)
val engineDependencies
get() = module {
singleOf(::Engine)
}
val storeDependencies
get() = module {
singleOf(::CharacterSheetStore)
singleOf(::CampaignStore)
}
val repositoryDependencies
val serviceDependencies
get() = module {
singleOf(::CharacterSheetRepository)
singleOf(::CharacterSheetService)
singleOf(::CampaignService)
}
val factoryDependencies

View file

@ -0,0 +1,49 @@
package com.pixelized.server.lwa.model.campaign
import com.pixelized.shared.lwa.model.campaign.Campaign
import com.pixelized.shared.lwa.model.campaign.CampaignJson
import com.pixelized.shared.lwa.model.campaign.CampaignJsonFactory
import com.pixelized.shared.lwa.protocol.payload.UpdatePlayerCharacteristicMessage
import com.pixelized.shared.lwa.usecase.CampaignUseCase
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn
class CampaignService(
private val store: CampaignStore,
private val factory: CampaignJsonFactory,
private val useCase: CampaignUseCase,
) {
private val scope = CoroutineScope(Dispatchers.IO + Job())
private val campaign = store.campaignFlow().stateIn(
scope = scope,
started = SharingStarted.Eagerly,
initialValue = Campaign.EMPTY,
)
fun campaign(): CampaignJson {
return campaign.value.let(factory::convertToJson)
}
suspend fun update(
message: UpdatePlayerCharacteristicMessage,
) {
// fetch all the current campaign character
val characters = campaign.value.characters.toMutableMap()
// update the corresponding character using the usecase
characters[message.characterId] = useCase.updateCharacteristic(
character = characters[message.characterId] ?: Campaign.CharacterInstance(
characteristic = emptyMap(),
usedSkill = emptyList(),
),
characteristic = message.characteristic,
value = message.value,
)
// save the campaign to the disk + update the flow.
store.save(
campaign = campaign.value.copy(characters = characters)
)
}
}

View file

@ -0,0 +1,72 @@
package com.pixelized.server.lwa.model.campaign
import com.pixelized.shared.lwa.campaignPath
import com.pixelized.shared.lwa.model.campaign.Campaign
import com.pixelized.shared.lwa.model.campaign.CampaignJson
import com.pixelized.shared.lwa.model.campaign.CampaignJsonFactory
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.io.File
class CampaignStore(
private val factory: CampaignJsonFactory,
private val json: Json,
) {
private val directory = File(campaignPath()).also { it.mkdirs() }
private val flow = MutableStateFlow(value = Campaign.EMPTY)
init {
val scope = CoroutineScope(Dispatchers.IO + Job())
scope.launch {
flow.value = load()
}
}
fun campaignFlow(): StateFlow<Campaign> = flow
suspend fun load(): Campaign {
return try {
val json = file().readText(charset = Charsets.UTF_8)
if (json.isBlank()) Campaign.EMPTY
val campaign = this.json.decodeFromString<CampaignJson>(json)
factory.convertFromJson(campaign)
} catch (exception: Exception) {
Campaign.EMPTY
}
}
suspend fun save(campaign: Campaign) {
// convert the data to json format
val json = try {
factory.convertToJson(data = campaign).let(json::encodeToString)
} catch (exception: Exception) {
throw JsonConversionException(root = exception)
}
// write the file
try {
val file = file()
file.writeText(
text = json,
charset = Charsets.UTF_8,
)
} catch (exception: Exception) {
throw FileWriteException(root = exception)
}
// Update the dataflow.
flow.value = campaign
}
sealed class CampaignStoreException(root: Exception) : Exception(root)
class JsonConversionException(root: Exception) : CampaignStoreException(root)
class FileWriteException(root: Exception) : CampaignStoreException(root)
private fun file(): File {
return File("${campaignPath()}campaign.json")
}
}

View file

@ -1,27 +0,0 @@
package com.pixelized.server.lwa.model.character
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
class CharacterSheetRepository(
store: CharacterSheetStore,
) {
private val scope = CoroutineScope(Dispatchers.IO + Job())
private val sheets = store.characterSheetFlow()
.stateIn(
scope = scope,
started = SharingStarted.Eagerly,
initialValue = emptyList()
)
fun characterSheetFlow(): StateFlow<List<CharacterSheet>> {
return sheets
}
}

View file

@ -0,0 +1,41 @@
package com.pixelized.server.lwa.model.character
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJson
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJsonFactory
import com.pixelized.shared.lwa.usecase.CharacterSheetUseCase
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn
class CharacterSheetService(
private val store: CharacterSheetStore,
private val factory: CharacterSheetJsonFactory,
private val useCase: CharacterSheetUseCase,
) {
private val scope = CoroutineScope(Dispatchers.IO + Job())
private val sheets = store.characterSheetFlow().stateIn(
scope = scope,
started = SharingStarted.Eagerly,
initialValue = emptyList()
)
fun character(): List<CharacterSheetJson> {
return sheets.value.map(factory::convertToJson)
}
fun characterSkillChange(
characterId: String,
skillId: String,
) {
val character = sheets.value.firstOrNull { it.id == characterId }
if (character != null) {
val update = useCase.updateSkillUsage(
character = character,
skillId = skillId,
)
store.save(sheet = update)
}
}
}

View file

@ -1,9 +1,9 @@
package com.pixelized.server.lwa.model.character
import com.pixelized.shared.lwa.characterStorePath
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheetJson
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheetJsonFactory
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJson
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJsonFactory
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@ -17,9 +17,9 @@ import java.text.Collator
class CharacterSheetStore(
private val factory: CharacterSheetJsonFactory,
private val jsonFormatter: Json,
private val json: Json,
) {
private val characterDirectory = File(characterStorePath()).also { it.mkdirs() }
private val directory = File(characterStorePath()).also { it.mkdirs() }
private val flow = MutableStateFlow<List<CharacterSheet>>(value = emptyList())
init {
@ -39,7 +39,7 @@ class CharacterSheetStore(
fun save(sheet: CharacterSheet) {
// convert the character sheet into json format.
val json = try {
factory.convertToJson(sheet = sheet).let(jsonFormatter::encodeToString)
factory.convertToJson(sheet = sheet).let(json::encodeToString)
} catch (exception: Exception) {
throw JsonConversionException(root = exception)
}
@ -85,7 +85,7 @@ class CharacterSheetStore(
JsonConversionException::class,
)
suspend fun load(): List<CharacterSheet> {
return characterDirectory
return directory
.listFiles()
?.mapNotNull { file ->
val json = try {
@ -98,7 +98,7 @@ class CharacterSheetStore(
return@mapNotNull null
}
try {
val sheet = jsonFormatter.decodeFromString<CharacterSheetJson>(json)
val sheet = this.json.decodeFromString<CharacterSheetJson>(json)
factory.convertFromJson(sheet)
} catch (exception: Exception) {
throw JsonConversionException(root = exception)

View file

@ -0,0 +1,38 @@
package com.pixelized.server.lwa.server
import com.pixelized.server.lwa.model.campaign.CampaignService
import com.pixelized.server.lwa.model.character.CharacterSheetService
import com.pixelized.shared.lwa.protocol.Message
import com.pixelized.shared.lwa.protocol.MessageType
import com.pixelized.shared.lwa.protocol.payload.UpdatePlayerCharacteristicMessage
import com.pixelized.shared.lwa.protocol.payload.UpdateSkillUsageMessage
import kotlinx.serialization.json.Json
class Engine(
private val characterService: CharacterSheetService,
private val campaignService: CampaignService,
private val json: Json,
) {
suspend fun handle(message: Message) {
println(message)
when (message.type) {
MessageType.Roll -> {
Unit // Nothing to do here.
}
MessageType.UpdateSkillUsage -> {
val data: UpdateSkillUsageMessage = json.decodeFromString(message.value)
characterService.characterSkillChange(
characterId = data.characterId,
skillId = data.skillId
)
}
MessageType.UpdatePlayerCharacteristic -> {
val data: UpdatePlayerCharacteristicMessage = json.decodeFromString(message.value)
campaignService.update(data)
}
}
}
}

View file

@ -3,9 +3,9 @@ package com.pixelized.server.lwa.server
import com.pixelized.server.lwa.extention.decodeFromFrame
import com.pixelized.server.lwa.extention.encodeToFrame
import com.pixelized.server.lwa.model.character.CharacterSheetRepository
import com.pixelized.server.lwa.model.campaign.CampaignService
import com.pixelized.server.lwa.model.character.CharacterSheetService
import com.pixelized.shared.lwa.SERVER_PORT
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheetJsonFactory
import com.pixelized.shared.lwa.protocol.Message
import com.pixelized.shared.lwa.sharedModuleDependencies
import io.ktor.http.ContentType
@ -65,24 +65,34 @@ class LocalServer {
masking = false
}
val repository by inject<CharacterSheetRepository>()
val factory by inject<CharacterSheetJsonFactory>()
val engine by inject<Engine>()
val characterService by inject<CharacterSheetService>()
val campaignService by inject<CampaignService>()
routing {
get(
path = "/",
body = {
call.respondText(contentType = ContentType.Text.Html) {
"<a href=\"http://127.0.0.1:16030/characters\">characters</a>"
"""<html><body><ul>
<li><a href="http://127.0.0.1:16030/characters">characters</a></li>
<li><a href="http://127.0.0.1:16030/campaign">campaign</a></li>
</ul></body></html>"""
}
}
)
get(
path = "/characters",
body = {
val body = repository.characterSheetFlow().value.map(factory::convertToJson)
call.respond(body)
call.respond(characterService.character())
},
)
get(
path = "/campaign",
body = {
call.respond(campaignService.campaign())
}
)
webSocket(
path = "/ws",
handler = {
@ -97,7 +107,8 @@ class LocalServer {
incoming.consumeEach { frame ->
if (frame is Frame.Text) {
val message = Json.decodeFromFrame(frame = frame)
println(message)
// log the message
engine.handle(message)
// broadcast to clients the message
outgoingMessageBuffer.emit(message)
}

View file

@ -1,6 +1,8 @@
package com.pixelized.shared.lwa
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheetJsonFactory
import com.pixelized.shared.lwa.model.campaign.CampaignJsonFactory
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJsonFactory
import com.pixelized.shared.lwa.usecase.CampaignUseCase
import com.pixelized.shared.lwa.usecase.CharacterSheetUseCase
import kotlinx.serialization.json.Json
import org.koin.core.module.dsl.factoryOf
@ -26,9 +28,11 @@ val toolsDependencies
val factoryDependencies
get() = module {
factoryOf(::CharacterSheetJsonFactory)
factoryOf(::CampaignJsonFactory)
}
val useCaseDependencies
get() = module {
factoryOf(::CharacterSheetUseCase)
factoryOf(::CampaignUseCase)
}

View file

@ -34,4 +34,13 @@ fun characterStorePath(
OperatingSystem.Windows -> "${storePath(os = os)}characters\\"
OperatingSystem.Macintosh -> "${storePath(os = os)}characters/"
}
}
fun campaignPath(
os: OperatingSystem = OperatingSystem.current,
): String {
return when (os) {
OperatingSystem.Windows -> "${storePath(os = os)}campaign\\"
OperatingSystem.Macintosh -> "${storePath(os = os)}campaign/"
}
}

View file

@ -0,0 +1,34 @@
package com.pixelized.shared.lwa.model.campaign
data class Campaign(
val characters: Map<String, CharacterInstance>,
) {
data class CharacterInstance(
val characteristic: Map<Characteristic, Int>,
val usedSkill: List<String>,
) {
enum class Characteristic {
Damage,
Power,
}
}
companion object {
val EMPTY = Campaign(
characters = emptyMap(),
)
}
}
fun Campaign.character(id: String): Campaign.CharacterInstance {
return characters[id] ?: Campaign.CharacterInstance(
characteristic = emptyMap(),
usedSkill = emptyList(),
)
}
val Campaign.CharacterInstance.damage
get() = characteristic[Campaign.CharacterInstance.Characteristic.Damage] ?: 0
val Campaign.CharacterInstance.power
get() = characteristic[Campaign.CharacterInstance.Characteristic.Power] ?: 0

View file

@ -1,4 +1,4 @@
package com.pixelized.shared.lwa.model.campaign.model
package com.pixelized.shared.lwa.model.campaign
import kotlinx.serialization.Serializable

View file

@ -0,0 +1,48 @@
package com.pixelized.shared.lwa.model.campaign
class CampaignJsonFactory {
fun convertFromJson(
json: CampaignJson,
): Campaign {
return when (json) {
is CampaignJsonV1 -> convertFromV1(json = json)
}
}
private fun convertFromV1(
json: CampaignJsonV1,
): Campaign {
return Campaign(
characters = json.characters.map { entry ->
entry.key to Campaign.CharacterInstance(
characteristic = entry.value.characteristic.map { char ->
when (char.key) {
CampaignJsonV1.CharacterInstanceJson.Characteristic.Damage -> Campaign.CharacterInstance.Characteristic.Damage
CampaignJsonV1.CharacterInstanceJson.Characteristic.Power -> Campaign.CharacterInstance.Characteristic.Power
} to char.value
}.toMap(),
usedSkill = entry.value.usedSkill,
)
}.toMap()
)
}
fun convertToJson(
data: Campaign,
): CampaignJson {
return CampaignJsonV1(
characters = data.characters.map { entry ->
entry.key to CampaignJsonV1.CharacterInstanceJson(
characteristic = entry.value.characteristic.map { char ->
when (char.key) {
Campaign.CharacterInstance.Characteristic.Damage -> CampaignJsonV1.CharacterInstanceJson.Characteristic.Damage
Campaign.CharacterInstance.Characteristic.Power -> CampaignJsonV1.CharacterInstanceJson.Characteristic.Power
} to char.value
}.toMap(),
usedSkill = entry.value.usedSkill,
)
}.toMap()
)
}
}

View file

@ -0,0 +1,20 @@
package com.pixelized.shared.lwa.model.campaign
import kotlinx.serialization.Serializable
@Serializable
data class CampaignJsonV1(
val characters: Map<String, CharacterInstanceJson>,
) : CampaignJson {
@Serializable
data class CharacterInstanceJson(
val characteristic: Map<Characteristic, Int>,
val usedSkill: List<String>,
) {
enum class Characteristic {
Damage,
Power,
}
}
}

View file

@ -1,9 +0,0 @@
package com.pixelized.shared.lwa.model.campaign
import com.pixelized.shared.lwa.model.campaign.model.CampaignFactory
class CampaignRepository(
private val factory: CampaignFactory,
) {
}

View file

@ -1,11 +0,0 @@
package com.pixelized.shared.lwa.model.campaign.model
data class Campaign(
val characters: List<CharacterInstance>,
) {
data class CharacterInstance(
val damage: Int,
val usedPower: Int,
val usedMovement: Int,
)
}

View file

@ -1,39 +0,0 @@
package com.pixelized.shared.lwa.model.campaign.model
class CampaignFactory {
fun convertFromJson(
json: CampaignJson,
): Campaign {
return when (json) {
is CampaignJsonV1 -> convertFromV1(json = json)
}
}
private fun convertFromV1(
json: CampaignJsonV1,
): Campaign {
return Campaign(
characters = json.characters.map {
Campaign.CharacterInstance(
damage = it.damage,
usedPower = it.usedPower,
usedMovement = it.usedMovement,
)
}
)
}
private fun convertToJson(
data: Campaign,
): CampaignJson {
return CampaignJsonV1(
characters = data.characters.map {
CampaignJsonV1.CharacterInstanceJson(
damage = it.damage,
usedPower = it.usedPower,
usedMovement = it.usedMovement,
)
}
)
}
}

View file

@ -1,15 +0,0 @@
package com.pixelized.shared.lwa.model.campaign.model
import kotlinx.serialization.Serializable
@Serializable
data class CampaignJsonV1(
val characters: List<CharacterInstanceJson>,
) : CampaignJson {
@Serializable
data class CharacterInstanceJson(
val damage: Int,
val usedPower: Int,
val usedMovement: Int,
)
}

View file

@ -1,10 +1,11 @@
package com.pixelized.shared.lwa.model.characterSheet.model
package com.pixelized.shared.lwa.model.characterSheet
data class CharacterSheet(
val id: String,
val name: String,
val portrait: String?,
val thumbnail: String?,
val level: Int,
// characteristics
val strength: Int,
val dexterity: Int,
@ -14,21 +15,12 @@ data class CharacterSheet(
val power: Int,
val charisma: Int,
// sub characteristics
val overrideMovement: Boolean,
val movement: Int,
val currentHp: Int,
val overrideMaxHp: Boolean,
val maxHp: Int,
val currentPp: Int,
val overrideMaxPP: Boolean,
val maxPp: Int,
val overrideDamageBonus: Boolean,
val hp: Int,
val pp: Int,
val damageBonus: String,
val overrideArmor: Boolean,
val armor: Int,
val overrideLearning: Boolean,
val learning: Int,
val overrideHpGrow: Boolean,
val hpGrow: Int,
// skills
val commonSkills: List<Skill>,
@ -43,7 +35,7 @@ data class CharacterSheet(
val description: String?,
val base: String,
val bonus: String?,
val level: String?,
val level: Int,
val occupation: Boolean,
val used: Boolean,
)
@ -51,6 +43,8 @@ data class CharacterSheet(
data class Roll(
val id: String,
val label: String,
val description: String?,
val canBeCritical: Boolean,
val roll: String,
)

View file

@ -1,4 +1,4 @@
package com.pixelized.shared.lwa.model.characterSheet.model
package com.pixelized.shared.lwa.model.characterSheet
import kotlinx.serialization.Serializable

View file

@ -1,4 +1,4 @@
package com.pixelized.shared.lwa.model.characterSheet.model
package com.pixelized.shared.lwa.model.characterSheet
import com.pixelized.shared.lwa.usecase.CharacterSheetUseCase
@ -22,6 +22,7 @@ class CharacterSheetJsonFactory(
name = json.name,
portrait = json.portrait,
thumbnail = json.thumbnail,
level = json.level,
strength = json.strength,
dexterity = json.dexterity,
constitution = json.constitution,
@ -29,28 +30,26 @@ class CharacterSheetJsonFactory(
intelligence = json.intelligence,
power = json.power,
charisma = json.charisma,
overrideMovement = json.movement != null,
movement = json.movement ?: defaultMovement(),
currentHp = json.currentHp,
overrideMaxHp = json.maxHp != null,
maxHp = json.maxHp ?: defaultMaxHp(
movement = defaultMovement(),
hp = defaultMaxHp(
constitution = json.constitution,
height = json.height,
level = json.level
),
currentPp = json.currentPP,
overrideMaxPP = json.maxPP != null,
maxPp = json.maxPP ?: defaultMaxPower(power = json.power),
overrideDamageBonus = json.damageBonus != null,
damageBonus = json.damageBonus ?: defaultDamageBonus(
pp = defaultMaxPower(
power = json.power,
),
damageBonus = defaultDamageBonus(
strength = json.strength,
height = json.height,
),
overrideArmor = json.armor != null,
armor = json.armor ?: defaultArmor(),
overrideLearning = json.learning != null,
learning = json.learning ?: defaultLearning(intelligence = json.intelligence),
overrideHpGrow = json.hpGrowf != null,
hpGrow = json.hpGrowf ?: defaultHpGrow(constitution = json.constitution),
armor = defaultArmor(),
learning = defaultLearning(
intelligence = json.intelligence,
),
hpGrow = defaultHpGrow(
constitution = json.constitution,
),
commonSkills = json.skills.map {
CharacterSheet.Skill(
id = it.id,
@ -91,6 +90,8 @@ class CharacterSheetJsonFactory(
CharacterSheet.Roll(
id = it.id,
label = it.label,
description = it.description,
canBeCritical = it.canBeCritical,
roll = it.roll,
)
},
@ -105,6 +106,7 @@ class CharacterSheetJsonFactory(
name = sheet.name,
thumbnail = sheet.thumbnail,
portrait = sheet.portrait,
level = sheet.level,
strength = sheet.strength,
dexterity = sheet.dexterity,
constitution = sheet.constitution,
@ -112,15 +114,6 @@ class CharacterSheetJsonFactory(
intelligence = sheet.intelligence,
power = sheet.power,
charisma = sheet.charisma,
movement = if (sheet.overrideMovement) sheet.movement else null,
currentHp = sheet.currentHp,
maxHp = if (sheet.overrideMaxHp) sheet.maxHp else null,
currentPP = sheet.currentPp,
maxPP = if (sheet.overrideMaxPP) sheet.maxPp else null,
damageBonus = if (sheet.overrideDamageBonus) sheet.damageBonus else null,
armor = if (sheet.overrideArmor) sheet.armor else null,
learning = if (sheet.overrideLearning) sheet.learning else null,
hpGrowf = if (sheet.overrideHpGrow) sheet.hpGrow else null,
skills = sheet.commonSkills.map {
CharacterSheetJsonV1.Skill(
id = it.id,
@ -161,6 +154,8 @@ class CharacterSheetJsonFactory(
CharacterSheetJsonV1.Roll(
id = it.id,
label = it.label,
description = it.description,
canBeCritical = it.canBeCritical,
roll = it.roll,
)
},

View file

@ -1,4 +1,4 @@
package com.pixelized.shared.lwa.model.characterSheet.model
package com.pixelized.shared.lwa.model.characterSheet
import kotlinx.serialization.Serializable
@ -8,6 +8,7 @@ data class CharacterSheetJsonV1(
val name: String,
val portrait: String?,
val thumbnail: String?,
val level: Int,
// characteristics
val strength: Int,
val dexterity: Int,
@ -16,16 +17,6 @@ data class CharacterSheetJsonV1(
val intelligence: Int,
val power: Int,
val charisma: Int,
// sub characteristics
val movement: Int?,
val currentHp: Int,
val maxHp: Int?,
val currentPP: Int,
val maxPP: Int?,
val damageBonus: String?,
val armor: Int?,
val learning: Int?,
val hpGrowf: Int?,
// skills
val skills: List<Skill>,
// occupations
@ -43,7 +34,7 @@ data class CharacterSheetJsonV1(
val description: String?,
val base: String,
val bonus: String?,
val level: String?,
val level: Int,
val occupation: Boolean,
val used: Boolean,
)
@ -52,6 +43,8 @@ data class CharacterSheetJsonV1(
data class Roll(
val id: String,
val label: String,
val description: String?,
val canBeCritical: Boolean,
val roll: String,
)
}

View file

@ -1,5 +1,7 @@
package com.pixelized.shared.lwa.protocol
enum class MessageType {
Roll
Roll,
UpdateSkillUsage,
UpdatePlayerCharacteristic,
}

View file

@ -0,0 +1,3 @@
package com.pixelized.shared.lwa.protocol.payload
sealed interface MessagePayload

View file

@ -1,4 +1,4 @@
package com.pixelized.shared.lwa.protocol.roll
package com.pixelized.shared.lwa.protocol.payload
import kotlinx.serialization.Serializable
@ -10,4 +10,4 @@ data class RollMessage(
val rollDifficulty: String?,
val rollValue: Int,
val rollSuccessLimit: Int?,
)
) : MessagePayload

View file

@ -0,0 +1,11 @@
package com.pixelized.shared.lwa.protocol.payload
import com.pixelized.shared.lwa.model.campaign.Campaign
import kotlinx.serialization.Serializable
@Serializable
data class UpdatePlayerCharacteristicMessage(
val characterId: String,
val characteristic: Campaign.CharacterInstance.Characteristic,
val value: Int,
) : MessagePayload

View file

@ -0,0 +1,9 @@
package com.pixelized.shared.lwa.protocol.payload
import kotlinx.serialization.Serializable
@Serializable
data class UpdateSkillUsageMessage(
val characterId: String,
val skillId: String,
) : MessagePayload

View file

@ -0,0 +1,18 @@
package com.pixelized.shared.lwa.usecase
import com.pixelized.shared.lwa.model.campaign.Campaign
class CampaignUseCase {
fun updateCharacteristic(
character: Campaign.CharacterInstance,
characteristic: Campaign.CharacterInstance.Characteristic,
value: Int,
): Campaign.CharacterInstance {
return character.copy(
characteristic = character.characteristic.toMutableMap().also {
it[characteristic] = value
}
)
}
}

View file

@ -1,5 +1,6 @@
package com.pixelized.shared.lwa.usecase
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet
import kotlin.math.ceil
import kotlin.math.max
@ -14,8 +15,10 @@ class CharacterSheetUseCase {
fun defaultMaxHp(
constitution: Int,
height: Int,
level: Int,
): Int {
return (ceil((constitution + height) / 2f).toInt())
val add = max(defaultHpGrow(constitution = constitution) * (level - 1), 0)
return (ceil((constitution + height) / 2f).toInt()) + add
}
fun defaultMaxPower(
@ -53,4 +56,33 @@ class CharacterSheetUseCase {
fun defaultHpGrow(constitution: Int): Int {
return (constitution / 3)
}
fun updateSkillUsage(
character: CharacterSheet,
skillId: String,
): CharacterSheet {
return character.copy(
commonSkills = character.commonSkills.map { skill ->
if (skill.id == skillId) {
skill.copy(used = skill.used.not())
} else {
skill
}
},
specialSkills = character.specialSkills.map { skill ->
if (skill.id == skillId) {
skill.copy(used = skill.used.not())
} else {
skill
}
},
magicSkills = character.magicSkills.map { skill ->
if (skill.id == skillId) {
skill.copy(used = skill.used.not())
} else {
skill
}
}
)
}
}