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.parser.word.WordParser
import com.pixelized.desktop.lwa.repository.alteration.AlterationRepository import com.pixelized.desktop.lwa.repository.alteration.AlterationRepository
import com.pixelized.desktop.lwa.repository.alteration.AlterationStore 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.CharacterSheetRepository
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetStore import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetStore
import com.pixelized.desktop.lwa.repository.network.NetworkRepository 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.network.NetworkViewModel
import com.pixelized.desktop.lwa.ui.screen.roll.RollViewModel import com.pixelized.desktop.lwa.ui.screen.roll.RollViewModel
import com.pixelized.desktop.lwa.ui.screen.rollhistory.RollHistoryViewModel import com.pixelized.desktop.lwa.ui.screen.rollhistory.RollHistoryViewModel
import com.pixelized.shared.lwa.model.campaign.CampaignRepository import com.pixelized.shared.lwa.model.campaign.CampaignJsonFactory
import com.pixelized.shared.lwa.model.campaign.model.CampaignFactory
import com.pixelized.shared.lwa.usecase.CharacterSheetUseCase import com.pixelized.shared.lwa.usecase.CharacterSheetUseCase
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.engine.HttpClientEngine import io.ktor.client.engine.HttpClientEngine
@ -78,6 +79,7 @@ val storeDependencies
singleOf(::CharacterSheetStore) singleOf(::CharacterSheetStore)
singleOf(::SettingsStore) singleOf(::SettingsStore)
singleOf(::AlterationStore) singleOf(::AlterationStore)
singleOf(::CampaignStore)
} }
val repositoryDependencies val repositoryDependencies
@ -97,7 +99,7 @@ val factoryDependencies
factoryOf(::NetworkFactory) factoryOf(::NetworkFactory)
factoryOf(::SkillFieldFactory) factoryOf(::SkillFieldFactory)
factoryOf(::SettingsFactory) factoryOf(::SettingsFactory)
factoryOf(::CampaignFactory) factoryOf(::CampaignJsonFactory)
} }
val viewModelDependencies 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.Expression
import com.pixelized.desktop.lwa.parser.expression.ExpressionParser import com.pixelized.desktop.lwa.parser.expression.ExpressionParser
import com.pixelized.desktop.lwa.parser.word.Word 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.max
import kotlin.math.min import kotlin.math.min
@ -32,9 +32,7 @@ class ExpressionUseCase(
val bonus = context.evaluate( val bonus = context.evaluate(
expression = skill.bonus?.let(expressionParser::parse), expression = skill.bonus?.let(expressionParser::parse),
) )
val level = context.evaluate( val level = max((skill.level - 1) * 5, 0)
expression = skill.level?.let(expressionParser::parse),
)
return max(base + bonus + level + alterations, 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.parser.expression.ExpressionParser
import com.pixelized.desktop.lwa.repository.alteration.model.Alteration import com.pixelized.desktop.lwa.repository.alteration.model.Alteration
import com.pixelized.desktop.lwa.repository.alteration.model.AlterationMetadata 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.CharacterSheet.CharacteristicId
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CharacteristicId.ARMOR import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.ARMOR
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CharacteristicId.DEX import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.DEX
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CharacteristicId.HEI import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.HEI
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CharacteristicId.MOV import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.MOV
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CharacteristicId.STR import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CharacteristicId.STR
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CommonSkillId.ACROBATICS_ID import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CommonSkillId.ACROBATICS_ID
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CommonSkillId.AID_ID import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CommonSkillId.AID_ID
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CommonSkillId.ATHLETICS_ID import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CommonSkillId.ATHLETICS_ID
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CommonSkillId.BARGAIN_ID import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CommonSkillId.BARGAIN_ID
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CommonSkillId.COMBAT_ID import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CommonSkillId.COMBAT_ID
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CommonSkillId.DISCRETION_ID import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CommonSkillId.DISCRETION_ID
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CommonSkillId.INTIMIDATION_ID import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CommonSkillId.INTIMIDATION_ID
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CommonSkillId.PERCEPTION_ID import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CommonSkillId.PERCEPTION_ID
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CommonSkillId.PERSUASION_ID import com.pixelized.shared.lwa.model.characterSheet.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.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.CharacterSheet.CommonSkillId.SPIEL_ID
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CommonSkillId.THROW_ID import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet.CommonSkillId.THROW_ID
class AlterationStore( class AlterationStore(
private val expressionParser: ExpressionParser, 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 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.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job

View file

@ -1,24 +1,31 @@
package com.pixelized.desktop.lwa.repository.characterSheet package com.pixelized.desktop.lwa.repository.characterSheet
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet import com.pixelized.desktop.lwa.repository.network.NetworkRepository
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheetJson import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheetJsonFactory 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.HttpClient
import io.ktor.client.call.body import io.ktor.client.call.body
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.request.get import io.ktor.client.request.get
import io.ktor.serialization.kotlinx.json.json
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
class CharacterSheetStore( class CharacterSheetStore(
private val network: NetworkRepository,
private val factory: CharacterSheetJsonFactory, private val factory: CharacterSheetJsonFactory,
private val useCase: CharacterSheetUseCase,
private val client: HttpClient, private val client: HttpClient,
private val json: Json,
) { ) {
private val flow = MutableStateFlow<List<CharacterSheet>>(value = emptyList()) private val flow = MutableStateFlow<List<CharacterSheet>>(value = emptyList())
@ -27,21 +34,24 @@ class CharacterSheetStore(
scope.launch { scope.launch {
flow.value = load() 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 characterSheetFlow(): StateFlow<List<CharacterSheet>> = flow
fun save(sheet: CharacterSheet) {
}
fun delete(id: String): Boolean {
return false
}
suspend fun load(): List<CharacterSheet> { suspend fun load(): List<CharacterSheet> {
val request: List<CharacterSheetJson> = client val request: List<CharacterSheetJson> = client
.get("http://pixelized.freeboxos.fr:16030/characters") .get("http://pixelized.freeboxos.fr:16030/characters") // TODO
.body() .body()
val data = request.map { val data = request.map {
factory.convertFromJson(json = it) factory.convertFromJson(json = it)
@ -49,8 +59,19 @@ class CharacterSheetStore(
return data return data
} }
sealed class CharacterSheetStoreException(root: Exception) : Exception(root) private fun updateCharacterSkillChange(
class JsonConversionException(root: Exception) : CharacterSheetStoreException(root) characterId: String,
class FileWriteException(root: Exception) : CharacterSheetStoreException(root) skillId: String,
class FileReadException(root: Exception) : CharacterSheetStoreException(root) ) {
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.SERVER_PORT
import com.pixelized.shared.lwa.protocol.Message import com.pixelized.shared.lwa.protocol.Message
import com.pixelized.shared.lwa.protocol.MessageType 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.client.HttpClient
import io.ktor.websocket.Frame import io.ktor.websocket.Frame
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -19,11 +23,13 @@ import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
class NetworkRepository( class NetworkRepository(
private val settingsRepository: SettingsRepository, private val settingsRepository: SettingsRepository,
private val client: HttpClient, private val client: HttpClient,
private val json: Json,
) { ) {
companion object { companion object {
const val DEFAULT_PORT = SERVER_PORT 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( suspend fun share(
playerName: String = settingsRepository.settings().playerName, playerName: String = settingsRepository.settings().playerName,
type: MessageType, 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.desktop.lwa.repository.network.NetworkRepository
import com.pixelized.shared.lwa.protocol.MessageType 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.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
@ -28,14 +28,6 @@ class RollHistoryRepository(
started = SharingStarted.Eagerly, started = SharingStarted.Eagerly,
) )
init {
scope.launch {
network.data.collect {
println(it)
}
}
}
suspend fun share( suspend fun share(
characterId: String, characterId: String,
skillLabel: String, skillLabel: String,

View file

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

View file

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

View file

@ -35,9 +35,9 @@ import org.jetbrains.compose.resources.painterResource
data class PlayerPortraitUio( data class PlayerPortraitUio(
val id: String, val id: String,
val portrait: String?, val portrait: String?,
val hp: Int, val damage: Int,
val maxHp: Int, val maxHp: Int,
val pp: Int, val usedPp: Int,
val maxPp: Int, val maxPp: Int,
) )
@ -96,7 +96,7 @@ fun PlayerPortrait(
Text( Text(
modifier = Modifier.padding(bottom = 2.dp), modifier = Modifier.padding(bottom = 2.dp),
style = MaterialTheme.typography.caption, style = MaterialTheme.typography.caption,
text = "${character.hp}/${character.maxHp}", text = "${character.maxHp - character.damage}/${character.maxHp}",
) )
} }
Row( Row(
@ -111,7 +111,7 @@ fun PlayerPortrait(
Text( Text(
modifier = Modifier.padding(bottom = 2.dp), modifier = Modifier.padding(bottom = 2.dp),
style = MaterialTheme.typography.caption, 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.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.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 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.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
class PlayerRibbonViewModel( class PlayerRibbonViewModel(
private val rollHistoryRepository: RollHistoryRepository, private val rollHistoryRepository: RollHistoryRepository,
characterRepository: CharacterSheetRepository, characterRepository: CharacterSheetRepository,
campaignRepository: CampaignRepository,
) : ViewModel() { ) : ViewModel() {
val characters: StateFlow<List<PlayerPortraitUio>> = characterRepository.characterSheetFlow() val characters: StateFlow<List<PlayerPortraitUio>> = combine(
.map { sheets -> characterRepository.characterSheetFlow(),
sheets.map { sheet -> campaignRepository.campaignFlow(),
PlayerPortraitUio( ) { sheets, campaign ->
id = sheet.id, sheets.map { sheet ->
portrait = sheet.thumbnail, val instance = campaign.character(id = sheet.id)
hp = sheet.currentHp, PlayerPortraitUio(
maxHp = sheet.maxHp, id = sheet.id,
pp = sheet.currentPp, portrait = sheet.thumbnail,
maxPp = sheet.maxPp, damage = instance.damage,
) maxHp = sheet.hp,
} usedPp = instance.power,
}.stateIn( maxPp = sheet.pp,
scope = viewModelScope, )
started = SharingStarted.Eagerly, }
initialValue = emptyList() }.stateIn(
) scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = emptyList()
)
private val rolls = hashMapOf<String, MutableState<PlayerPortraitRollUio?>>() private val rolls = hashMapOf<String, MutableState<PlayerPortraitRollUio?>>()

View file

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

View file

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

View file

@ -9,27 +9,40 @@ import androidx.compose.ui.text.input.TextFieldValue
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope 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.alteration.AlterationRepository
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.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.CharacterSheetDeleteConfirmationDialogUio
import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.dialog.DiminishedStatDialogUio 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.ui.screen.characterSheet.detail.dialog.StatChangeDialogUio
import com.pixelized.desktop.lwa.utils.extention.collectAsState 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.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.Res
import lwacharactersheet.composeapp.generated.resources.character_sheet__diminished__label 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__hit_point
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__sub_characteristics__power_point import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__sub_characteristics__power_point
import org.jetbrains.compose.resources.getString import org.jetbrains.compose.resources.getString
import kotlin.math.max
import kotlin.math.min
private typealias CSDCDialogUio = CharacterSheetDeleteConfirmationDialogUio private typealias CSDCDialogUio = CharacterSheetDeleteConfirmationDialogUio
class CharacterSheetViewModel( 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 alteration: AlterationRepository,
private val factory: CharacterSheetFactory, private val factory: CharacterSheetFactory,
savedStateHandle: SavedStateHandle, savedStateHandle: SavedStateHandle,
@ -49,7 +62,7 @@ class CharacterSheetViewModel(
private val _diminishedDialog = mutableStateOf<DiminishedStatDialogUio?>(null) private val _diminishedDialog = mutableStateOf<DiminishedStatDialogUio?>(null)
val diminishedDialog: State<DiminishedStatDialogUio?> get() = _diminishedDialog 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?> val diminishedValue: State<Int?>
@Composable @Composable
get() = diminishedValueFlow.collectAsState { it -> get() = diminishedValueFlow.collectAsState { it ->
@ -57,53 +70,46 @@ class CharacterSheetViewModel(
} }
private val sheetFlow = combine( private val sheetFlow = combine(
repository.characterSheetFlow(id = argument.id), characterRepository.characterSheetFlow(id = argument.id),
repository.characterDiminishedFlow(id = argument.id), campaignRepository.campaignFlow(),
alteration.alterations(characterId = argument.id), alteration.alterations(characterId = argument.id),
transform = { sheet, diminished, alterations -> transform = { sheet, campaign, alterations ->
factory.convertToUio(sheet = sheet, diminished = diminished, alterations = alterations) factory.convertToUio(
sheet = sheet,
campaign = campaign,
alterations = alterations
)
}, },
).stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = null,
) )
val sheet: State<CharacterSheetPageUio?> val sheet: State<CharacterSheetPageUio?>
@Composable @Composable
get() = sheetFlow.collectAsState( get() = sheetFlow.collectAsState()
initial = null,
context = viewModelScope.coroutineContext,
)
fun toggleWolf() { fun toggleWolf() {
alteration.toggle(argument.id, "65e37d32-3031-4bf8-9369-d2c45d2efac0") alteration.toggle(argument.id, "65e37d32-3031-4bf8-9369-d2c45d2efac0")
} }
fun deleteCharacter(id: String) { fun deleteCharacter(id: String) {
repository.delete(id = id) characterRepository.delete(id = id)
} }
fun onUseSkill(skill: CharacterSheetPageUio.Node) { fun onUseSkill(skill: CharacterSheetPageUio.Node) {
repository.characterSheetFlow(id = argument.id).value?.let { sheet -> viewModelScope.launch {
network.share(
val skills = sheet.commonSkills.map { payload = UpdateSkillUsageMessage(
if (it.label == skill.label) it.copy(used = it.used.not()) else it characterId = argument.id,
} skillId = skill.id,
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,
) )
) )
} }
} }
fun showConfirmCharacterDeletionDialog() { fun showConfirmCharacterDeletionDialog() {
repository.characterSheetFlow(id = argument.id).value?.let { sheet -> characterRepository.characterSheetFlow(id = argument.id).value?.let { sheet ->
_displayDeleteConfirmationDialog.value = CharacterSheetDeleteConfirmationDialogUio( _displayDeleteConfirmationDialog.value = CharacterSheetDeleteConfirmationDialogUio(
id = sheet.id, id = sheet.id,
name = sheet.name, name = sheet.name,
@ -116,35 +122,36 @@ class CharacterSheetViewModel(
} }
suspend fun showSubCharacteristicDialog(id: String) { 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) { _statChangeDialog.value = when (id) {
CharacterSheet.CharacteristicId.HP -> { CharacterSheet.CharacteristicId.HP -> {
val value = mutableStateOf( val value = mutableStateOf(
"${sheet.currentHp}".let { "${sheet.hp - instance.damage}".let {
TextFieldValue(text = it, selection = TextRange(it.length)) TextFieldValue(text = it, selection = TextRange(it.length))
} }
) )
StatChangeDialogUio( StatChangeDialogUio(
id = CharacterSheet.CharacteristicId.HP, id = Characteristic.Damage,
label = getString(resource = Res.string.character_sheet_edit__sub_characteristics__hit_point), label = getString(resource = Res.string.character_sheet_edit__sub_characteristics__hit_point),
value = { value.value }, value = { value.value },
onValueChange = { value.value = it }, onValueChange = { value.value = it },
maxValue = "${sheet.maxHp}", maxValue = "${sheet.hp}",
) )
} }
CharacterSheet.CharacteristicId.PP -> { CharacterSheet.CharacteristicId.PP -> {
val value = mutableStateOf( val value = mutableStateOf(
"${sheet.currentPp}".let { "${sheet.power - instance.power}".let {
TextFieldValue(text = it, selection = TextRange(it.length)) TextFieldValue(text = it, selection = TextRange(it.length))
} }
) )
StatChangeDialogUio( StatChangeDialogUio(
id = CharacterSheet.CharacteristicId.PP, id = Characteristic.Power,
label = getString(resource = Res.string.character_sheet_edit__sub_characteristics__power_point), label = getString(resource = Res.string.character_sheet_edit__sub_characteristics__power_point),
value = { value.value }, value = { value.value },
onValueChange = { value.value = it }, onValueChange = { value.value = it },
maxValue = "${sheet.maxPp}", maxValue = "${sheet.power}",
) )
} }
@ -158,29 +165,24 @@ class CharacterSheetViewModel(
} }
fun changeSubCharacteristic( fun changeSubCharacteristic(
characteristicId: String, characteristicId: Characteristic,
value: Int, value: Int,
) { ) {
val sheet = repository.characterSheetFlow(id = argument.id).value viewModelScope.launch {
val updated = when (characteristicId) { val sheet = characterRepository.characterSheetFlow(id = argument.id).value
CharacterSheet.CharacteristicId.HP -> sheet?.copy( if (sheet != null) {
currentHp = max( network.share(
0, payload = UpdatePlayerCharacteristicMessage(
min(sheet.maxHp, value) 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() { suspend fun showDiminishedDialog() {
val diminished = repository.characterDiminishedFlow(id = argument.id).value val diminished = characterRepository.characterDiminishedFlow(id = argument.id).value
val textFieldValue = val textFieldValue =
mutableStateOf(TextFieldValue("$diminished", selection = TextRange(index = 0))) mutableStateOf(TextFieldValue("$diminished", selection = TextRange(index = 0)))
_diminishedDialog.value = DiminishedStatDialogUio( _diminishedDialog.value = DiminishedStatDialogUio(
@ -215,7 +217,7 @@ class CharacterSheetViewModel(
fun changeDiminished(dialog: DiminishedStatDialogUio) { fun changeDiminished(dialog: DiminishedStatDialogUio) {
val value = dialog.value().text.toIntOrNull() ?: 0 val value = dialog.value().text.toIntOrNull() ?: 0
repository.setDiminishedForCharacter( characterRepository.setDiminishedForCharacter(
id = dialog.id, id = dialog.id,
diminished = value, 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.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.composable.decoratedBox.DecoratedBox 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.Res
import lwacharactersheet.composeapp.generated.resources.dialog__cancel_action import lwacharactersheet.composeapp.generated.resources.dialog__cancel_action
import lwacharactersheet.composeapp.generated.resources.dialog__confirm_action import lwacharactersheet.composeapp.generated.resources.dialog__confirm_action
@ -49,7 +50,7 @@ import org.jetbrains.compose.resources.stringResource
@Stable @Stable
data class StatChangeDialogUio( data class StatChangeDialogUio(
val id: String, val id: Campaign.CharacterInstance.Characteristic,
val label: String, val label: String,
val value: () -> TextFieldValue, val value: () -> TextFieldValue,
val onValueChange: (TextFieldValue) -> Unit, 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.Composable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember 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
import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.CharacterSheetPageUio.Characteristic 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.ActionFieldUio
import com.pixelized.desktop.lwa.ui.screen.characterSheet.edit.composable.BaseSkillFieldUio 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.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 com.pixelized.shared.lwa.usecase.CharacterSheetUseCase
import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__actions__action_label 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__special_title
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__spiel 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__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 org.jetbrains.compose.resources.getString
import java.util.UUID import java.util.UUID
import kotlin.math.max import kotlin.math.max
@ -60,56 +53,57 @@ class CharacterSheetEditFactory(
currentSheet: CharacterSheet?, currentSheet: CharacterSheet?,
editedSheet: CharacterSheetEditPageUio, editedSheet: CharacterSheetEditPageUio,
): CharacterSheet { ): 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( return CharacterSheet(
id = editedSheet.id, id = editedSheet.id,
name = editedSheet.name.value.value, name = editedSheet.name.value.value,
portrait = currentSheet?.portrait, portrait = currentSheet?.portrait,
thumbnail = currentSheet?.thumbnail, thumbnail = currentSheet?.thumbnail,
strength = editedSheet.strength.unpack()?.toIntOrNull() level = level,
?: currentSheet?.strength strength = strength,
?: 0, dexterity = dexterity,
dexterity = editedSheet.dexterity.unpack()?.toIntOrNull() constitution = constitution,
?: currentSheet?.dexterity height = height,
?: 0, intelligence = intelligence,
constitution = editedSheet.constitution.unpack()?.toIntOrNull() power = power,
?: currentSheet?.constitution charisma = charisma,
?: 0, hp = characterSheetUseCase.defaultMaxHp(
height = editedSheet.height.unpack()?.toIntOrNull() constitution = constitution,
?: currentSheet?.height height = height,
?: 0, level = level
intelligence = editedSheet.intelligence.unpack()?.toIntOrNull() ),
?: currentSheet?.intelligence pp = characterSheetUseCase.defaultMaxPower(power = power),
?: 0, movement = characterSheetUseCase.defaultMovement(),
power = editedSheet.power.unpack()?.toIntOrNull() damageBonus = characterSheetUseCase.defaultDamageBonus(
?: currentSheet?.power strength = strength,
?: 0, height = height
charisma = editedSheet.charisma.unpack()?.toIntOrNull() ),
?: currentSheet?.charisma armor = characterSheetUseCase.defaultArmor(),
?: 0, learning = characterSheetUseCase.defaultLearning(intelligence = intelligence),
overrideMovement = editedSheet.movement.value.value.value.isNotBlank(), hpGrow = characterSheetUseCase.defaultHpGrow(constitution = constitution),
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,
commonSkills = editedSheet.commonSkills.map { editedSkill -> commonSkills = editedSheet.commonSkills.map { editedSkill ->
val currentSkill = currentSheet?.commonSkills?.firstOrNull { val currentSkill = currentSheet?.commonSkills?.firstOrNull {
it.id == editedSkill.id it.id == editedSkill.id
@ -120,7 +114,7 @@ class CharacterSheetEditFactory(
description = currentSkill?.description, description = currentSkill?.description,
base = "${editedSkill.base.value}", base = "${editedSkill.base.value}",
bonus = editedSkill.bonus.value.value.takeIf { it.isNotBlank() }, 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, occupation = editedSkill.option.checked.value,
used = currentSkill?.used ?: false, used = currentSkill?.used ?: false,
) )
@ -135,7 +129,7 @@ class CharacterSheetEditFactory(
description = editedSkill.description.value.value, description = editedSkill.description.value.value,
base = editedSkill.base.value.value, base = editedSkill.base.value.value,
bonus = editedSkill.bonus.value.value.takeIf { it.isNotBlank() }, 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, occupation = editedSkill.options.occupation,
used = currentSkill?.used ?: false, used = currentSkill?.used ?: false,
) )
@ -150,7 +144,7 @@ class CharacterSheetEditFactory(
description = editedSkill.description.value.value, description = editedSkill.description.value.value,
base = editedSkill.base.value.value, base = editedSkill.base.value.value,
bonus = editedSkill.bonus.value.value.takeIf { it.isNotBlank() }, 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, occupation = editedSkill.options.occupation,
used = currentSkill?.used ?: false, used = currentSkill?.used ?: false,
) )
@ -159,6 +153,8 @@ class CharacterSheetEditFactory(
CharacterSheet.Roll( CharacterSheet.Roll(
id = it.id, id = it.id,
label = it.label.value.value, label = it.label.value.value,
description = null, // TODO
canBeCritical = false, // TODO
roll = it.action.value.value, roll = it.action.value.value,
) )
}, },
@ -244,77 +240,6 @@ class CharacterSheetEditFactory(
intelligence = int, intelligence = int,
power = pow, power = pow,
charisma = cha, 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( commonSkills = listOf(
createBaseSkill( createBaseSkill(
sheet = sheet, sheet = sheet,
@ -421,7 +346,7 @@ class CharacterSheetEditFactory(
labelValue = skill.label, labelValue = skill.label,
baseValue = skill.base, baseValue = skill.base,
bonusValue = skill.bonus ?: "", bonusValue = skill.bonus ?: "",
levelValue = skill.level ?: "", levelValue = skill.level.takeIf { it > 0 }?.toString() ?: "",
options = run { options = run {
val current = sheet.specialSkills.firstOrNull { it.id == skill.id } val current = sheet.specialSkills.firstOrNull { it.id == skill.id }
listOf( listOf(
@ -439,7 +364,7 @@ class CharacterSheetEditFactory(
labelValue = skill.label, labelValue = skill.label,
baseValue = skill.base, baseValue = skill.base,
bonusValue = skill.bonus ?: "", bonusValue = skill.bonus ?: "",
levelValue = skill.level ?: "", levelValue = skill.level.takeIf { it > 0 }?.toString() ?: "",
options = run { options = run {
val current = sheet.magicSkills.firstOrNull { it.id == skill.id } val current = sheet.magicSkills.firstOrNull { it.id == skill.id }
listOf( listOf(
@ -487,7 +412,7 @@ class CharacterSheetEditFactory(
), ),
level = skillFieldFactory.createWrapper( level = skillFieldFactory.createWrapper(
label = mutableStateOf(getString(Res.string.character_sheet_edit__skills__level_label)), 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), option = skillFieldFactory.occupationOption(skill?.occupation ?: false),
) )

View file

@ -67,13 +67,6 @@ data class CharacterSheetEditPageUio(
val intelligence: SimpleFieldUio, val intelligence: SimpleFieldUio,
val power: SimpleFieldUio, val power: SimpleFieldUio,
val charisma: 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 commonSkills: List<BaseSkillFieldUio>,
val specialSkills: List<SkillFieldUio>, val specialSkills: List<SkillFieldUio>,
val magicSkills: List<SkillFieldUio>, val magicSkills: List<SkillFieldUio>,
@ -89,17 +82,6 @@ data class CharacterSheetEditPageUio(
power, power,
charisma, charisma,
) )
val subCharacteristics
get() = listOf(
movement,
maxHp,
maxPp,
damageBonus,
armor,
learning,
hpGrow,
)
} }
@Composable @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( 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.repository.roll_history.RollHistoryRepository
import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.CharacterSheetPageUio import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.CharacterSheetPageUio
import com.pixelized.desktop.lwa.ui.screen.roll.DifficultyUio.Difficulty 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.Job
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay

View file

@ -6,7 +6,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
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 com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch import kotlinx.coroutines.launch

View file

@ -10,6 +10,7 @@ koin = "4.0.0"
turtle = "0.5.0" turtle = "0.5.0"
logback = "1.5.11" logback = "1.5.11"
coil = "3.1.0" coil = "3.1.0"
filament-android = "1.17.1"
[plugins] [plugins]
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" } 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" } logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }
coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" } 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.websockets)
implementation(libs.ktor.server.negotiation) implementation(libs.ktor.server.negotiation)
implementation(libs.ktor.serialization.json) 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.model.character.CharacterSheetStore
import com.pixelized.server.lwa.server.Engine
import org.koin.core.module.dsl.singleOf import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module import org.koin.dsl.module
@ -9,17 +12,25 @@ val serverModuleDependencies
factoryDependencies, factoryDependencies,
useCaseDependencies, useCaseDependencies,
storeDependencies, storeDependencies,
repositoryDependencies, serviceDependencies,
engineDependencies,
) )
val engineDependencies
get() = module {
singleOf(::Engine)
}
val storeDependencies val storeDependencies
get() = module { get() = module {
singleOf(::CharacterSheetStore) singleOf(::CharacterSheetStore)
singleOf(::CampaignStore)
} }
val repositoryDependencies val serviceDependencies
get() = module { get() = module {
singleOf(::CharacterSheetRepository) singleOf(::CharacterSheetService)
singleOf(::CampaignService)
} }
val factoryDependencies 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 package com.pixelized.server.lwa.model.character
import com.pixelized.shared.lwa.characterStorePath import com.pixelized.shared.lwa.characterStorePath
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheetJson import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJson
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheetJsonFactory import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJsonFactory
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -17,9 +17,9 @@ import java.text.Collator
class CharacterSheetStore( class CharacterSheetStore(
private val factory: CharacterSheetJsonFactory, 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()) private val flow = MutableStateFlow<List<CharacterSheet>>(value = emptyList())
init { init {
@ -39,7 +39,7 @@ class CharacterSheetStore(
fun save(sheet: CharacterSheet) { fun save(sheet: CharacterSheet) {
// convert the character sheet into json format. // convert the character sheet into json format.
val json = try { val json = try {
factory.convertToJson(sheet = sheet).let(jsonFormatter::encodeToString) factory.convertToJson(sheet = sheet).let(json::encodeToString)
} catch (exception: Exception) { } catch (exception: Exception) {
throw JsonConversionException(root = exception) throw JsonConversionException(root = exception)
} }
@ -85,7 +85,7 @@ class CharacterSheetStore(
JsonConversionException::class, JsonConversionException::class,
) )
suspend fun load(): List<CharacterSheet> { suspend fun load(): List<CharacterSheet> {
return characterDirectory return directory
.listFiles() .listFiles()
?.mapNotNull { file -> ?.mapNotNull { file ->
val json = try { val json = try {
@ -98,7 +98,7 @@ class CharacterSheetStore(
return@mapNotNull null return@mapNotNull null
} }
try { try {
val sheet = jsonFormatter.decodeFromString<CharacterSheetJson>(json) val sheet = this.json.decodeFromString<CharacterSheetJson>(json)
factory.convertFromJson(sheet) factory.convertFromJson(sheet)
} catch (exception: Exception) { } catch (exception: Exception) {
throw JsonConversionException(root = 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.decodeFromFrame
import com.pixelized.server.lwa.extention.encodeToFrame 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.SERVER_PORT
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheetJsonFactory
import com.pixelized.shared.lwa.protocol.Message import com.pixelized.shared.lwa.protocol.Message
import com.pixelized.shared.lwa.sharedModuleDependencies import com.pixelized.shared.lwa.sharedModuleDependencies
import io.ktor.http.ContentType import io.ktor.http.ContentType
@ -65,24 +65,34 @@ class LocalServer {
masking = false masking = false
} }
val repository by inject<CharacterSheetRepository>() val engine by inject<Engine>()
val factory by inject<CharacterSheetJsonFactory>() val characterService by inject<CharacterSheetService>()
val campaignService by inject<CampaignService>()
routing { routing {
get( get(
path = "/", path = "/",
body = { body = {
call.respondText(contentType = ContentType.Text.Html) { 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( get(
path = "/characters", path = "/characters",
body = { body = {
val body = repository.characterSheetFlow().value.map(factory::convertToJson) call.respond(characterService.character())
call.respond(body)
}, },
) )
get(
path = "/campaign",
body = {
call.respond(campaignService.campaign())
}
)
webSocket( webSocket(
path = "/ws", path = "/ws",
handler = { handler = {
@ -97,7 +107,8 @@ class LocalServer {
incoming.consumeEach { frame -> incoming.consumeEach { frame ->
if (frame is Frame.Text) { if (frame is Frame.Text) {
val message = Json.decodeFromFrame(frame = frame) val message = Json.decodeFromFrame(frame = frame)
println(message) // log the message
engine.handle(message)
// broadcast to clients the message // broadcast to clients the message
outgoingMessageBuffer.emit(message) outgoingMessageBuffer.emit(message)
} }

View file

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

View file

@ -34,4 +34,13 @@ fun characterStorePath(
OperatingSystem.Windows -> "${storePath(os = os)}characters\\" OperatingSystem.Windows -> "${storePath(os = os)}characters\\"
OperatingSystem.Macintosh -> "${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 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( data class CharacterSheet(
val id: String, val id: String,
val name: String, val name: String,
val portrait: String?, val portrait: String?,
val thumbnail: String?, val thumbnail: String?,
val level: Int,
// characteristics // characteristics
val strength: Int, val strength: Int,
val dexterity: Int, val dexterity: Int,
@ -14,21 +15,12 @@ data class CharacterSheet(
val power: Int, val power: Int,
val charisma: Int, val charisma: Int,
// sub characteristics // sub characteristics
val overrideMovement: Boolean,
val movement: Int, val movement: Int,
val currentHp: Int, val hp: Int,
val overrideMaxHp: Boolean, val pp: Int,
val maxHp: Int,
val currentPp: Int,
val overrideMaxPP: Boolean,
val maxPp: Int,
val overrideDamageBonus: Boolean,
val damageBonus: String, val damageBonus: String,
val overrideArmor: Boolean,
val armor: Int, val armor: Int,
val overrideLearning: Boolean,
val learning: Int, val learning: Int,
val overrideHpGrow: Boolean,
val hpGrow: Int, val hpGrow: Int,
// skills // skills
val commonSkills: List<Skill>, val commonSkills: List<Skill>,
@ -43,7 +35,7 @@ data class CharacterSheet(
val description: String?, val description: String?,
val base: String, val base: String,
val bonus: String?, val bonus: String?,
val level: String?, val level: Int,
val occupation: Boolean, val occupation: Boolean,
val used: Boolean, val used: Boolean,
) )
@ -51,6 +43,8 @@ data class CharacterSheet(
data class Roll( data class Roll(
val id: String, val id: String,
val label: String, val label: String,
val description: String?,
val canBeCritical: Boolean,
val roll: String, 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 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 import com.pixelized.shared.lwa.usecase.CharacterSheetUseCase
@ -22,6 +22,7 @@ class CharacterSheetJsonFactory(
name = json.name, name = json.name,
portrait = json.portrait, portrait = json.portrait,
thumbnail = json.thumbnail, thumbnail = json.thumbnail,
level = json.level,
strength = json.strength, strength = json.strength,
dexterity = json.dexterity, dexterity = json.dexterity,
constitution = json.constitution, constitution = json.constitution,
@ -29,28 +30,26 @@ class CharacterSheetJsonFactory(
intelligence = json.intelligence, intelligence = json.intelligence,
power = json.power, power = json.power,
charisma = json.charisma, charisma = json.charisma,
overrideMovement = json.movement != null, movement = defaultMovement(),
movement = json.movement ?: defaultMovement(), hp = defaultMaxHp(
currentHp = json.currentHp,
overrideMaxHp = json.maxHp != null,
maxHp = json.maxHp ?: defaultMaxHp(
constitution = json.constitution, constitution = json.constitution,
height = json.height, height = json.height,
level = json.level
), ),
currentPp = json.currentPP, pp = defaultMaxPower(
overrideMaxPP = json.maxPP != null, power = json.power,
maxPp = json.maxPP ?: defaultMaxPower(power = json.power), ),
overrideDamageBonus = json.damageBonus != null, damageBonus = defaultDamageBonus(
damageBonus = json.damageBonus ?: defaultDamageBonus(
strength = json.strength, strength = json.strength,
height = json.height, height = json.height,
), ),
overrideArmor = json.armor != null, armor = defaultArmor(),
armor = json.armor ?: defaultArmor(), learning = defaultLearning(
overrideLearning = json.learning != null, intelligence = json.intelligence,
learning = json.learning ?: defaultLearning(intelligence = json.intelligence), ),
overrideHpGrow = json.hpGrowf != null, hpGrow = defaultHpGrow(
hpGrow = json.hpGrowf ?: defaultHpGrow(constitution = json.constitution), constitution = json.constitution,
),
commonSkills = json.skills.map { commonSkills = json.skills.map {
CharacterSheet.Skill( CharacterSheet.Skill(
id = it.id, id = it.id,
@ -91,6 +90,8 @@ class CharacterSheetJsonFactory(
CharacterSheet.Roll( CharacterSheet.Roll(
id = it.id, id = it.id,
label = it.label, label = it.label,
description = it.description,
canBeCritical = it.canBeCritical,
roll = it.roll, roll = it.roll,
) )
}, },
@ -105,6 +106,7 @@ class CharacterSheetJsonFactory(
name = sheet.name, name = sheet.name,
thumbnail = sheet.thumbnail, thumbnail = sheet.thumbnail,
portrait = sheet.portrait, portrait = sheet.portrait,
level = sheet.level,
strength = sheet.strength, strength = sheet.strength,
dexterity = sheet.dexterity, dexterity = sheet.dexterity,
constitution = sheet.constitution, constitution = sheet.constitution,
@ -112,15 +114,6 @@ class CharacterSheetJsonFactory(
intelligence = sheet.intelligence, intelligence = sheet.intelligence,
power = sheet.power, power = sheet.power,
charisma = sheet.charisma, 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 { skills = sheet.commonSkills.map {
CharacterSheetJsonV1.Skill( CharacterSheetJsonV1.Skill(
id = it.id, id = it.id,
@ -161,6 +154,8 @@ class CharacterSheetJsonFactory(
CharacterSheetJsonV1.Roll( CharacterSheetJsonV1.Roll(
id = it.id, id = it.id,
label = it.label, label = it.label,
description = it.description,
canBeCritical = it.canBeCritical,
roll = it.roll, 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 import kotlinx.serialization.Serializable
@ -8,6 +8,7 @@ data class CharacterSheetJsonV1(
val name: String, val name: String,
val portrait: String?, val portrait: String?,
val thumbnail: String?, val thumbnail: String?,
val level: Int,
// characteristics // characteristics
val strength: Int, val strength: Int,
val dexterity: Int, val dexterity: Int,
@ -16,16 +17,6 @@ data class CharacterSheetJsonV1(
val intelligence: Int, val intelligence: Int,
val power: Int, val power: Int,
val charisma: 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 // skills
val skills: List<Skill>, val skills: List<Skill>,
// occupations // occupations
@ -43,7 +34,7 @@ data class CharacterSheetJsonV1(
val description: String?, val description: String?,
val base: String, val base: String,
val bonus: String?, val bonus: String?,
val level: String?, val level: Int,
val occupation: Boolean, val occupation: Boolean,
val used: Boolean, val used: Boolean,
) )
@ -52,6 +43,8 @@ data class CharacterSheetJsonV1(
data class Roll( data class Roll(
val id: String, val id: String,
val label: String, val label: String,
val description: String?,
val canBeCritical: Boolean,
val roll: String, val roll: String,
) )
} }

View file

@ -1,5 +1,7 @@
package com.pixelized.shared.lwa.protocol package com.pixelized.shared.lwa.protocol
enum class MessageType { 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 import kotlinx.serialization.Serializable
@ -10,4 +10,4 @@ data class RollMessage(
val rollDifficulty: String?, val rollDifficulty: String?,
val rollValue: Int, val rollValue: Int,
val rollSuccessLimit: 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 package com.pixelized.shared.lwa.usecase
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet
import kotlin.math.ceil import kotlin.math.ceil
import kotlin.math.max import kotlin.math.max
@ -14,8 +15,10 @@ class CharacterSheetUseCase {
fun defaultMaxHp( fun defaultMaxHp(
constitution: Int, constitution: Int,
height: Int, height: Int,
level: Int,
): 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( fun defaultMaxPower(
@ -53,4 +56,33 @@ class CharacterSheetUseCase {
fun defaultHpGrow(constitution: Int): Int { fun defaultHpGrow(constitution: Int): Int {
return (constitution / 3) 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
}
}
)
}
} }