Refactor project data to allow server handling.

This commit is contained in:
Thomas Andres Gomez 2025-02-22 12:54:19 +01:00
parent 3c8eecdab5
commit 1e5f0d88ae
58 changed files with 742 additions and 469 deletions

View file

@ -14,6 +14,8 @@ kotlin {
val desktopMain by getting val desktopMain by getting
commonMain.dependencies { commonMain.dependencies {
// common
implementation(projects.shared)
// compose // compose
implementation(compose.runtime) implementation(compose.runtime)
implementation(compose.foundation) implementation(compose.foundation)
@ -26,19 +28,18 @@ kotlin {
implementation(libs.androidx.lifecycle.runtime.compose) implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.androidx.navigation.compose) implementation(libs.androidx.navigation.compose)
// injection // injection
api(libs.koin.core)
implementation(libs.koin.compose) implementation(libs.koin.compose)
implementation(libs.koin.compose.viewmodel) implementation(libs.koin.compose.viewmodel)
// composable component. // composable component.
implementation(libs.coil.compose) implementation(libs.coil.compose)
implementation(libs.coil.network) implementation(libs.coil.network)
// common
implementation(projects.shared)
// network // network
implementation(libs.kotlinx.serialization.json) implementation(libs.kotlinx.serialization.json)
implementation(libs.ktor.serialization.json)
implementation(libs.ktor.client.core) implementation(libs.ktor.client.core)
implementation(libs.ktor.client.cio) implementation(libs.ktor.client.okhttp)
implementation(libs.ktor.client.websockets) implementation(libs.ktor.client.websockets)
implementation(libs.ktor.client.negotiation)
// shell // shell
implementation(libs.turtle) implementation(libs.turtle)
} }

View file

@ -10,4 +10,8 @@
-keep class kotlinx.coroutines.** { *; } -keep class kotlinx.coroutines.** { *; }
# OkHttp comming from COIL. # OkHttp comming from COIL.
-dontwarn okhttp3.internal.platform.** -dontwarn okhttp3.internal.platform.**
# Serialization
-keep class io.ktor.serialization.kotlinx.json.** { *; }
-keep class com.pixelized.shared.lwa.model.** { *; }

View file

@ -44,6 +44,7 @@ import com.pixelized.desktop.lwa.repository.network.NetworkRepository.Status
import com.pixelized.desktop.lwa.ui.navigation.screen.MainNavHost import com.pixelized.desktop.lwa.ui.navigation.screen.MainNavHost
import com.pixelized.desktop.lwa.ui.screen.campaign.player.CampaignScreen import com.pixelized.desktop.lwa.ui.screen.campaign.player.CampaignScreen
import com.pixelized.desktop.lwa.ui.screen.characterSheet.CharacterSheetMainNavHost import com.pixelized.desktop.lwa.ui.screen.characterSheet.CharacterSheetMainNavHost
import com.pixelized.desktop.lwa.ui.screen.network.NetworkViewModel
import com.pixelized.desktop.lwa.ui.screen.rollhistory.RollHistoryPage import com.pixelized.desktop.lwa.ui.screen.rollhistory.RollHistoryPage
import com.pixelized.desktop.lwa.ui.theme.LwaTheme import com.pixelized.desktop.lwa.ui.theme.LwaTheme
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -53,6 +54,7 @@ import lwacharactersheet.composeapp.generated.resources.network__disconnect__mes
import org.jetbrains.compose.resources.getString import org.jetbrains.compose.resources.getString
import org.jetbrains.compose.ui.tooling.preview.Preview import org.jetbrains.compose.ui.tooling.preview.Preview
import org.koin.compose.koinInject import org.koin.compose.koinInject
import org.koin.compose.viewmodel.koinViewModel
val LocalWindowController = compositionLocalOf<WindowController> { val LocalWindowController = compositionLocalOf<WindowController> {
error("Local Window Controller is not yet ready") error("Local Window Controller is not yet ready")
@ -125,8 +127,8 @@ fun ApplicationScope.App() {
} }
}, },
content = { content = {
// MainNavHost() MainNavHost()
CampaignScreen() // CampaignScreen()
} }
) )
NetworkSnackHandler( NetworkSnackHandler(

View file

@ -1,21 +1,24 @@
package com.pixelized.desktop.lwa package com.pixelized.desktop.lwa
import com.pixelized.desktop.lwa.business.CharacterSheetUseCase import com.pixelized.desktop.lwa.business.ExpressionUseCase
import com.pixelized.desktop.lwa.business.RollUseCase import com.pixelized.desktop.lwa.business.RollUseCase
import com.pixelized.desktop.lwa.business.SettingsUseCase import com.pixelized.desktop.lwa.business.SettingsUseCase
import com.pixelized.desktop.lwa.business.SkillStepUseCase import com.pixelized.desktop.lwa.business.SkillStepUseCase
import com.pixelized.desktop.lwa.business.ExpressionUseCase import com.pixelized.desktop.lwa.parser.dice.DiceParser
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetJsonFactory import com.pixelized.desktop.lwa.parser.expression.ExpressionParser
import com.pixelized.desktop.lwa.parser.word.WordParser
import com.pixelized.desktop.lwa.repository.alteration.AlterationRepository
import com.pixelized.desktop.lwa.repository.alteration.AlterationStore
import com.pixelized.desktop.lwa.repository.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.characterSheet.SkillDescriptionFactory
import com.pixelized.desktop.lwa.repository.network.NetworkRepository import com.pixelized.desktop.lwa.repository.network.NetworkRepository
import com.pixelized.desktop.lwa.repository.roll_history.RollHistoryRepository import com.pixelized.desktop.lwa.repository.roll_history.RollHistoryRepository
import com.pixelized.desktop.lwa.repository.settings.SettingsFactory import com.pixelized.desktop.lwa.repository.settings.SettingsFactory
import com.pixelized.desktop.lwa.repository.settings.SettingsRepository import com.pixelized.desktop.lwa.repository.settings.SettingsRepository
import com.pixelized.desktop.lwa.repository.alteration.AlterationStore
import com.pixelized.desktop.lwa.repository.alteration.AlterationRepository
import com.pixelized.desktop.lwa.repository.settings.SettingsStore import com.pixelized.desktop.lwa.repository.settings.SettingsStore
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDiminishedViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.PlayerRibbonViewModel
import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.CharacterSheetFactory import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.CharacterSheetFactory
import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.CharacterSheetViewModel import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.CharacterSheetViewModel
import com.pixelized.desktop.lwa.ui.screen.characterSheet.edit.CharacterSheetEditFactory import com.pixelized.desktop.lwa.ui.screen.characterSheet.edit.CharacterSheetEditFactory
@ -26,32 +29,48 @@ 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.desktop.lwa.parser.dice.DiceParser import com.pixelized.shared.lwa.model.campaign.CampaignRepository
import com.pixelized.desktop.lwa.parser.word.WordParser import com.pixelized.shared.lwa.model.campaign.model.CampaignFactory
import com.pixelized.desktop.lwa.parser.expression.ExpressionParser import com.pixelized.shared.lwa.usecase.CharacterSheetUseCase
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailViewModel import io.ktor.client.HttpClient
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDiminishedViewModel import io.ktor.client.engine.HttpClientEngine
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.PlayerRibbonViewModel import io.ktor.client.engine.okhttp.OkHttp
import kotlinx.serialization.json.Json import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.websocket.WebSockets
import io.ktor.serialization.kotlinx.json.json
import org.koin.core.module.dsl.factoryOf import org.koin.core.module.dsl.factoryOf
import org.koin.core.module.dsl.singleOf import org.koin.core.module.dsl.singleOf
import org.koin.core.module.dsl.viewModelOf import org.koin.core.module.dsl.viewModelOf
import org.koin.dsl.module import org.koin.dsl.module
val moduleDependencies val appModuleDependencies
get() = listOf( get() = listOf(
toolsDependencies,
parserDependencies, parserDependencies,
factoryDependencies, factoryDependencies,
useCaseDependencies, useCaseDependencies,
storeDependencies, storeDependencies,
repositoryDependencies, repositoryDependencies,
viewModelDependencies, viewModelDependencies,
toolsDependencies,
) )
val toolsDependencies val toolsDependencies
get() = module { get() = module {
factory { Json { explicitNulls = false } } single<HttpClientEngine> {
OkHttp.create()
}
single {
HttpClient(
engine = get()
) {
install(WebSockets) {
pingIntervalMillis = 20_000
}
install(ContentNegotiation) {
json(get())
}
}
}
} }
val storeDependencies val storeDependencies
@ -68,17 +87,17 @@ val repositoryDependencies
singleOf(::RollHistoryRepository) singleOf(::RollHistoryRepository)
singleOf(::SettingsRepository) singleOf(::SettingsRepository)
singleOf(::AlterationRepository) singleOf(::AlterationRepository)
singleOf(::CampaignRepository)
} }
val factoryDependencies val factoryDependencies
get() = module { get() = module {
factoryOf(::CharacterSheetFactory) factoryOf(::CharacterSheetFactory)
factoryOf(::CharacterSheetEditFactory) factoryOf(::CharacterSheetEditFactory)
factoryOf(::CharacterSheetJsonFactory)
factoryOf(::NetworkFactory) factoryOf(::NetworkFactory)
factoryOf(::SkillFieldFactory) factoryOf(::SkillFieldFactory)
factoryOf(::SkillDescriptionFactory)
factoryOf(::SettingsFactory) factoryOf(::SettingsFactory)
factoryOf(::CampaignFactory)
} }
val viewModelDependencies val viewModelDependencies
@ -106,6 +125,6 @@ val useCaseDependencies
factoryOf(::SkillStepUseCase) factoryOf(::SkillStepUseCase)
factoryOf(::RollUseCase) factoryOf(::RollUseCase)
factoryOf(::ExpressionUseCase) factoryOf(::ExpressionUseCase)
factoryOf(::CharacterSheetUseCase)
factoryOf(::SettingsUseCase) factoryOf(::SettingsUseCase)
factoryOf(::CharacterSheetUseCase)
} }

View file

@ -4,8 +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.desktop.lwa.repository.alteration.model.FieldAlteration import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet
import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheet
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min

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

View file

@ -1,6 +1,6 @@
package com.pixelized.desktop.lwa.repository.characterSheet package com.pixelized.desktop.lwa.repository.characterSheet
import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheet import com.pixelized.shared.lwa.model.characterSheet.model.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
@ -53,11 +53,11 @@ class CharacterSheetRepository(
} }
fun save(characterSheet: CharacterSheet) { fun save(characterSheet: CharacterSheet) {
store.save(sheet = characterSheet) // store.save(sheet = characterSheet)
} }
fun delete(id: String) { fun delete(id: String) {
store.delete(id = id) // store.delete(id = id)
} }
fun setDiminishedForCharacter(id: String, diminished: Int) { fun setDiminishedForCharacter(id: String, diminished: Int) {
@ -65,5 +65,4 @@ class CharacterSheetRepository(
this[id] = diminished this[id] = diminished
} }
} }
} }

View file

@ -1,24 +1,25 @@
package com.pixelized.desktop.lwa.repository.characterSheet package com.pixelized.desktop.lwa.repository.characterSheet
import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheet import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet
import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheetJson import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheetJson
import com.pixelized.desktop.lwa.repository.characterStorePath import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheetJsonFactory
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.request.get
import io.ktor.serialization.kotlinx.json.json
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.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.launch import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.io.File
import java.text.Collator
class CharacterSheetStore( class CharacterSheetStore(
private val factory: CharacterSheetJsonFactory, private val factory: CharacterSheetJsonFactory,
private val jsonFormatter: Json, private val client: HttpClient,
) { ) {
private val characterDirectory = File(characterStorePath()).also { it.mkdirs() }
private val flow = MutableStateFlow<List<CharacterSheet>>(value = emptyList()) private val flow = MutableStateFlow<List<CharacterSheet>>(value = emptyList())
init { init {
@ -30,85 +31,22 @@ class CharacterSheetStore(
fun characterSheetFlow(): StateFlow<List<CharacterSheet>> = flow fun characterSheetFlow(): StateFlow<List<CharacterSheet>> = flow
@Throws(
CharacterSheetStoreException::class,
FileWriteException::class,
JsonConversionException::class,
)
fun save(sheet: CharacterSheet) { fun save(sheet: CharacterSheet) {
// convert the character sheet into json format.
val json = try {
factory.convertToJson(sheet = sheet).let(jsonFormatter::encodeToString)
} catch (exception: Exception) {
throw JsonConversionException(root = exception)
}
// write the character file.
try {
val file = characterSheetFile(id = sheet.id)
file.writeText(
text = json,
charset = Charsets.UTF_8,
)
} catch (exception: Exception) {
throw FileWriteException(root = exception)
}
// Update the dataflow.
flow.value = flow.value
.toMutableList()
.also { data ->
val index = data.indexOfFirst { it.id == sheet.id }
if (index >= 0) {
data[index] = sheet
} else {
data.add(sheet)
}
}
.sortedWith(compareBy(Collator.getInstance()) { it.name })
} }
fun delete(id: String): Boolean { fun delete(id: String): Boolean {
val file = characterSheetFile(id = id) return false
flow.value = flow.value.toMutableList()
.also { data ->
data.removeIf { it.id == id }
}
.sortedBy {
it.name
}
return file.delete()
} }
@Throws(
CharacterSheetStoreException::class,
FileReadException::class,
JsonConversionException::class,
)
suspend fun load(): List<CharacterSheet> { suspend fun load(): List<CharacterSheet> {
return characterDirectory val request: List<CharacterSheetJson> = client
.listFiles() .get("http://pixelized.freeboxos.fr:16030/characters")
?.mapNotNull { file -> .body()
val json = try { val data = request.map {
file.readText(charset = Charsets.UTF_8) factory.convertFromJson(json = it)
} catch (exception: Exception) { }
throw FileReadException(root = exception) return data
}
// Guard, if the json is blank no character have been save, ignore this file.
if (json.isBlank()) {
return@mapNotNull null
}
try {
val sheet = jsonFormatter.decodeFromString<CharacterSheetJson>(json)
factory.convertFromJson(sheet)
} catch (exception: Exception) {
throw JsonConversionException(root = exception)
}
}
?.sortedWith(compareBy(Collator.getInstance()) { it.name })
?: emptyList()
}
private fun characterSheetFile(id: String): File {
return File("${characterStorePath()}${id}.json")
} }
sealed class CharacterSheetStoreException(root: Exception) : Exception(root) sealed class CharacterSheetStoreException(root: Exception) : Exception(root)

View file

@ -1,47 +0,0 @@
package com.pixelized.desktop.lwa.repository.characterSheet
import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheet
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.tooltip__skills__acrobatics
import lwacharactersheet.composeapp.generated.resources.tooltip__skills__aid
import lwacharactersheet.composeapp.generated.resources.tooltip__skills__athletics
import lwacharactersheet.composeapp.generated.resources.tooltip__skills__bargain
import lwacharactersheet.composeapp.generated.resources.tooltip__skills__combat
import lwacharactersheet.composeapp.generated.resources.tooltip__skills__discretion
import lwacharactersheet.composeapp.generated.resources.tooltip__skills__dodge
import lwacharactersheet.composeapp.generated.resources.tooltip__skills__empathy
import lwacharactersheet.composeapp.generated.resources.tooltip__skills__grab
import lwacharactersheet.composeapp.generated.resources.tooltip__skills__intimidation
import lwacharactersheet.composeapp.generated.resources.tooltip__skills__perception
import lwacharactersheet.composeapp.generated.resources.tooltip__skills__persuasion
import lwacharactersheet.composeapp.generated.resources.tooltip__skills__search
import lwacharactersheet.composeapp.generated.resources.tooltip__skills__sleight_of_hand
import lwacharactersheet.composeapp.generated.resources.tooltip__skills__spiel
import lwacharactersheet.composeapp.generated.resources.tooltip__skills__throw
import org.jetbrains.compose.resources.getString
class SkillDescriptionFactory {
suspend fun baseSkillDescription(id: String): String? {
return when (id) {
CharacterSheet.CommonSkillId.COMBAT_ID -> getString(Res.string.tooltip__skills__combat)
CharacterSheet.CommonSkillId.DODGE_ID -> getString(Res.string.tooltip__skills__dodge)
CharacterSheet.CommonSkillId.GRAB_ID -> getString(Res.string.tooltip__skills__grab)
CharacterSheet.CommonSkillId.THROW_ID -> getString(Res.string.tooltip__skills__throw)
CharacterSheet.CommonSkillId.ATHLETICS_ID -> getString(Res.string.tooltip__skills__athletics)
CharacterSheet.CommonSkillId.ACROBATICS_ID -> getString(Res.string.tooltip__skills__acrobatics)
CharacterSheet.CommonSkillId.PERCEPTION_ID -> getString(Res.string.tooltip__skills__perception)
CharacterSheet.CommonSkillId.SEARCH_ID -> getString(Res.string.tooltip__skills__search)
CharacterSheet.CommonSkillId.EMPATHY_ID -> getString(Res.string.tooltip__skills__empathy)
CharacterSheet.CommonSkillId.PERSUASION_ID -> getString(Res.string.tooltip__skills__persuasion)
CharacterSheet.CommonSkillId.INTIMIDATION_ID -> getString(Res.string.tooltip__skills__intimidation)
CharacterSheet.CommonSkillId.SPIEL_ID -> getString(Res.string.tooltip__skills__spiel)
CharacterSheet.CommonSkillId.BARGAIN_ID -> getString(Res.string.tooltip__skills__bargain)
CharacterSheet.CommonSkillId.DISCRETION_ID -> getString(Res.string.tooltip__skills__discretion)
CharacterSheet.CommonSkillId.SLEIGHT_OF_HAND_ID -> getString(Res.string.tooltip__skills__sleight_of_hand)
CharacterSheet.CommonSkillId.AID_ID -> getString(Res.string.tooltip__skills__aid)
else -> null
}
}
}

View file

@ -1,16 +1,12 @@
package com.pixelized.desktop.lwa.repository.network package com.pixelized.desktop.lwa.repository.network
//import com.pixelized.desktop.lwa.repository.network.helper.server
//import io.ktor.server.engine.EmbeddedServer
//import io.ktor.server.netty.NettyApplicationEngine
import com.pixelized.desktop.lwa.repository.network.helper.client
import com.pixelized.desktop.lwa.repository.network.helper.connectWebSocket import com.pixelized.desktop.lwa.repository.network.helper.connectWebSocket
import com.pixelized.desktop.lwa.repository.settings.SettingsRepository import com.pixelized.desktop.lwa.repository.settings.SettingsRepository
import com.pixelized.desktop.lwa.utils.extention.decodeFromFrame import com.pixelized.desktop.lwa.utils.extention.decodeFromFrame
import com.pixelized.desktop.lwa.utils.extention.encodeToFrame import com.pixelized.desktop.lwa.utils.extention.encodeToFrame
import com.pixelized.server.lwa.SERVER_PORT import com.pixelized.shared.lwa.SERVER_PORT
import com.pixelized.server.lwa.protocol.Message import com.pixelized.shared.lwa.protocol.Message
import com.pixelized.server.lwa.protocol.MessageType import com.pixelized.shared.lwa.protocol.MessageType
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
@ -25,10 +21,9 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
typealias Client = HttpClient
class NetworkRepository( class NetworkRepository(
private val settingsRepository: SettingsRepository, private val settingsRepository: SettingsRepository,
private val client: HttpClient,
) { ) {
companion object { companion object {
const val DEFAULT_PORT = SERVER_PORT const val DEFAULT_PORT = SERVER_PORT
@ -37,7 +32,6 @@ class NetworkRepository(
private val scope = CoroutineScope(Dispatchers.IO) private val scope = CoroutineScope(Dispatchers.IO)
private var networkJob: Job? = null private var networkJob: Job? = null
private var client: Client? = null
private val outgoingMessageBuffer = MutableSharedFlow<Message>() private val outgoingMessageBuffer = MutableSharedFlow<Message>()
private val incomingMessageBuffer = MutableSharedFlow<Message>() private val incomingMessageBuffer = MutableSharedFlow<Message>()
@ -46,7 +40,6 @@ class NetworkRepository(
private val _status = MutableStateFlow(Status.DISCONNECTED) private val _status = MutableStateFlow(Status.DISCONNECTED)
val status: StateFlow<Status> get() = _status val status: StateFlow<Status> get() = _status
fun connect( fun connect(
host: String, host: String,
port: Int, port: Int,
@ -54,12 +47,10 @@ class NetworkRepository(
onFailure: (Exception) -> Unit = { }, onFailure: (Exception) -> Unit = { },
onClose: () -> Unit = { }, onClose: () -> Unit = { },
) { ) {
client = client()
networkJob?.cancel() networkJob?.cancel()
networkJob = scope.launch { networkJob = scope.launch {
try { try {
client?.connectWebSocket(host = host, port = port) { client.connectWebSocket(host = host, port = port) {
_status.value = Status.CONNECTED _status.value = Status.CONNECTED
onConnect() onConnect()
@ -93,7 +84,7 @@ class NetworkRepository(
fun disconnect() { fun disconnect() {
networkJob?.cancel() networkJob?.cancel()
scope.launch { scope.launch {
client?.close() client.close()
} }
} }

View file

@ -1,26 +1,15 @@
package com.pixelized.desktop.lwa.repository.network.helper package com.pixelized.desktop.lwa.repository.network.helper
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import io.ktor.client.plugins.websocket.DefaultClientWebSocketSession import io.ktor.client.plugins.websocket.DefaultClientWebSocketSession
import io.ktor.client.plugins.websocket.WebSockets
import io.ktor.client.plugins.websocket.webSocket import io.ktor.client.plugins.websocket.webSocket
import io.ktor.http.HttpMethod import io.ktor.http.HttpMethod
// https://ktor.io/docs/client-websockets.html#handle-session // https://ktor.io/docs/client-websockets.html#handle-session
fun client(): HttpClient {
val client = HttpClient(CIO) {
install(WebSockets) {
pingIntervalMillis = 20_000
}
}
return client
}
suspend fun HttpClient.connectWebSocket( suspend fun HttpClient.connectWebSocket(
host: String, host: String,
port: Int, port: Int,
block: suspend DefaultClientWebSocketSession.() -> Unit block: suspend DefaultClientWebSocketSession.() -> Unit,
) { ) {
webSocket( webSocket(
method = HttpMethod.Get, method = HttpMethod.Get,

View file

@ -1,8 +1,8 @@
package com.pixelized.desktop.lwa.repository.roll_history 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.server.lwa.protocol.MessageType import com.pixelized.shared.lwa.protocol.MessageType
import com.pixelized.server.lwa.protocol.roll.RollMessage import com.pixelized.shared.lwa.protocol.roll.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

View file

@ -3,7 +3,7 @@ package com.pixelized.desktop.lwa.repository.settings
import com.pixelized.desktop.lwa.business.SettingsUseCase import com.pixelized.desktop.lwa.business.SettingsUseCase
import com.pixelized.desktop.lwa.repository.settings.model.Settings import com.pixelized.desktop.lwa.repository.settings.model.Settings
import com.pixelized.desktop.lwa.repository.settings.model.SettingsJson import com.pixelized.desktop.lwa.repository.settings.model.SettingsJson
import com.pixelized.desktop.lwa.repository.storePath import com.pixelized.shared.lwa.storePath
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

@ -2,6 +2,7 @@ package com.pixelized.desktop.lwa.ui.navigation.screen
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.compositionLocalOf
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
@ -9,6 +10,8 @@ import androidx.navigation.compose.rememberNavController
import com.pixelized.desktop.lwa.ui.navigation.screen.destination.MainDestination import com.pixelized.desktop.lwa.ui.navigation.screen.destination.MainDestination
import com.pixelized.desktop.lwa.ui.navigation.screen.destination.composableMainPage import com.pixelized.desktop.lwa.ui.navigation.screen.destination.composableMainPage
import com.pixelized.desktop.lwa.ui.navigation.screen.destination.composableNetworkPage import com.pixelized.desktop.lwa.ui.navigation.screen.destination.composableNetworkPage
import com.pixelized.desktop.lwa.ui.screen.network.NetworkViewModel
import org.koin.compose.viewmodel.koinViewModel
val LocalScreenController = compositionLocalOf<NavHostController> { val LocalScreenController = compositionLocalOf<NavHostController> {
error("MainNavHost controller is not yet ready") error("MainNavHost controller is not yet ready")
@ -18,7 +21,12 @@ val LocalScreenController = compositionLocalOf<NavHostController> {
fun MainNavHost( fun MainNavHost(
controller: NavHostController = rememberNavController(), controller: NavHostController = rememberNavController(),
startDestination: String = MainDestination.navigationRoute(), startDestination: String = MainDestination.navigationRoute(),
networkViewModel: NetworkViewModel = koinViewModel(),
) { ) {
LaunchedEffect(Unit) {
networkViewModel.connect()
}
CompositionLocalProvider( CompositionLocalProvider(
LocalScreenController provides controller, LocalScreenController provides controller,
) { ) {

View file

@ -83,9 +83,7 @@ fun CampaignScreen(
}, },
leftOverlay = { leftOverlay = {
PlayerRibbon( PlayerRibbon(
modifier = Modifier modifier = Modifier.fillMaxHeight(),
.padding(all = 8.dp)
.fillMaxHeight(),
onCharacter = { onCharacter = {
characterDetailViewModel.showCharacter(id = it) characterDetailViewModel.showCharacter(id = it)
}, },

View file

@ -141,7 +141,7 @@ private fun Background(
) { ) {
Surface( Surface(
modifier = modifier.fillMaxSize(), modifier = modifier.fillMaxSize(),
color = MaterialTheme.lwa.color.elevatedSurface, color = MaterialTheme.lwa.colorScheme.elevatedSurface,
) { ) {
// Image( // Image(
// modifier = Modifier.fillMaxSize().drawWithContent { // modifier = Modifier.fillMaxSize().drawWithContent {
@ -200,7 +200,7 @@ private fun CharacterHeader(
) { ) {
Icon( Icon(
painter = painterResource(Res.drawable.ic_close_24dp), painter = painterResource(Res.drawable.ic_close_24dp),
tint = MaterialTheme.lwa.color.base.primary, tint = MaterialTheme.lwa.colorScheme.base.primary,
contentDescription = null, contentDescription = null,
) )
} }
@ -220,7 +220,7 @@ private fun CharacterHeader(
Text( Text(
modifier = Modifier.alignByBaseline(), modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.h6, style = MaterialTheme.typography.h6,
color = MaterialTheme.lwa.color.base.primary, color = MaterialTheme.lwa.colorScheme.base.primary,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
text = dynDetail.value.hp, text = dynDetail.value.hp,
) )
@ -243,7 +243,7 @@ private fun CharacterHeader(
Text( Text(
modifier = Modifier.alignByBaseline(), modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.h6, style = MaterialTheme.typography.h6,
color = MaterialTheme.lwa.color.base.primary, color = MaterialTheme.lwa.colorScheme.base.primary,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
text = dynDetail.value.pp, text = dynDetail.value.pp,
) )

View file

@ -2,7 +2,6 @@ package com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@ -20,13 +19,13 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.FilterQuality import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage import coil3.compose.AsyncImage
import com.pixelized.desktop.lwa.ui.composable.decoratedBox.DecoratedBox import com.pixelized.desktop.lwa.ui.composable.decoratedBox.DecoratedBox
import com.pixelized.desktop.lwa.ui.theme.lwa
import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.ic_heart_24dp import lwacharactersheet.composeapp.generated.resources.ic_heart_24dp
import lwacharactersheet.composeapp.generated.resources.ic_water_drop_24dp import lwacharactersheet.composeapp.generated.resources.ic_water_drop_24dp
@ -42,19 +41,15 @@ data class PlayerPortraitUio(
val maxPp: Int, val maxPp: Int,
) )
object PlayerPortrait {
object Default {
val size = DpSize(96.dp, 128.dp)
}
}
@Composable @Composable
fun PlayerPortrait( fun PlayerPortrait(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
size: DpSize = PlayerPortrait.Default.size, size: DpSize,
character: PlayerPortraitUio, character: PlayerPortraitUio,
onCharacter: (id: String) -> Unit, onCharacter: (id: String) -> Unit,
) { ) {
val colorScheme = MaterialTheme.lwa.colorScheme
DecoratedBox( DecoratedBox(
modifier = modifier modifier = modifier
.size(size = size) .size(size = size)
@ -76,11 +71,11 @@ fun PlayerPortrait(
drawRect( drawRect(
brush = Brush.verticalGradient( brush = Brush.verticalGradient(
listOf( listOf(
Color.Black.copy(alpha = 0.0f), colorScheme.elevatedSurface.copy(alpha = 0.0f),
Color.Black.copy(alpha = 0.0f), colorScheme.elevatedSurface.copy(alpha = 0.0f),
Color.Black.copy(alpha = 0.0f), colorScheme.elevatedSurface.copy(alpha = 0.0f),
Color.Black.copy(alpha = 0.5f), colorScheme.elevatedSurface.copy(alpha = 0.5f),
Color.Black.copy(alpha = 0.8f), colorScheme.elevatedSurface.copy(alpha = 0.8f),
) )
) )
) )

View file

@ -9,28 +9,45 @@ import androidx.compose.animation.core.spring
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith import androidx.compose.animation.togetherWith
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.PointerMatcher
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.onClick
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Shadow import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.PointerButton
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.ic_d20_24dp import lwacharactersheet.composeapp.generated.resources.ic_d20_24dp
import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.painterResource
@Stable
data class PlayerPortraitRollUio(
val characterId: String,
val value: Int?,
)
@Stable @Stable
data class PlayerPortraitRollAnimation( data class PlayerPortraitRollAnimation(
val alpha: Animatable<Float, AnimationVector1D> = Animatable(0f), val alpha: Animatable<Float, AnimationVector1D> = Animatable(0f),
@ -38,13 +55,19 @@ data class PlayerPortraitRollAnimation(
val scale: Animatable<Float, AnimationVector1D> = Animatable(1f), val scale: Animatable<Float, AnimationVector1D> = Animatable(1f),
) )
@OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun PlayerPortraitRoll( fun PlayerPortraitRoll(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
value: Int?, size: DpSize,
value: PlayerPortraitRollUio?,
onLeftClick: (PlayerPortraitRollUio) -> Unit,
onRightClick: (PlayerPortraitRollUio) -> Unit,
) { ) {
AnimatedContent( AnimatedContent(
modifier = modifier.graphicsLayer { clip = false }, modifier = modifier
.size(size = size)
.graphicsLayer { clip = false },
targetState = value, targetState = value,
transitionSpec = { transitionSpec = {
val enter = fadeIn() val enter = fadeIn()
@ -61,33 +84,41 @@ fun PlayerPortraitRoll(
}, },
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
when (it) { if (it != null) {
null -> Unit Icon(
else -> { modifier = Modifier
Icon( .graphicsLayer {
modifier = Modifier this.alpha = 0.8f
.graphicsLayer { this.rotationZ = animation.rotation.value
this.alpha = 0.8f }
this.rotationZ = animation.rotation.value .fillMaxWidth()
} .aspectRatio(1f)
.size(48.dp), .padding(all = 8.dp)
painter = painterResource(Res.drawable.ic_d20_24dp), .clip(shape = CircleShape)
tint = MaterialTheme.colors.primary, .onClick(
contentDescription = null, matcher = PointerMatcher.mouse(PointerButton.Secondary),
) onClick = { onRightClick(it) },
Text( ).clickable {
style = MaterialTheme.typography.h5.copy( onLeftClick(it)
shadow = Shadow( }
color = MaterialTheme.colors.surface, .padding(all = 8.dp),
offset = Offset.Zero, painter = painterResource(Res.drawable.ic_d20_24dp),
blurRadius = 8f, tint = MaterialTheme.colors.primary,
) contentDescription = null,
), )
textAlign = TextAlign.Center, Text(
color = MaterialTheme.colors.onSurface, style = MaterialTheme.typography.h5.copy(
text = it.toString() shadow = Shadow(
) color = MaterialTheme.colors.surface,
} offset = Offset.Zero,
blurRadius = 8f,
)
),
fontWeight = FontWeight.SemiBold,
textAlign = TextAlign.Center,
color = MaterialTheme.colors.onSurface,
text = it.value.toString()
)
} }
} }
} }

View file

@ -1,26 +1,36 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon package com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import org.koin.compose.viewmodel.koinViewModel import org.koin.compose.viewmodel.koinViewModel
object PlayerRibbon {
object Default {
val size = DpSize(96.dp, 128.dp)
}
}
@Composable @Composable
fun PlayerRibbon( fun PlayerRibbon(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
playerRibbonViewModel: PlayerRibbonViewModel = koinViewModel(), playerRibbonViewModel: PlayerRibbonViewModel = koinViewModel(),
padding: PaddingValues = PaddingValues(all = 8.dp),
onCharacter: (id: String) -> Unit, onCharacter: (id: String) -> Unit,
) { ) {
val characters = playerRibbonViewModel.characters.collectAsState() val characters = playerRibbonViewModel.characters.collectAsState()
LazyColumn( LazyColumn(
modifier = modifier, modifier = modifier,
contentPadding = padding,
verticalArrangement = Arrangement.spacedBy(space = 8.dp) verticalArrangement = Arrangement.spacedBy(space = 8.dp)
) { ) {
items( items(
@ -29,15 +39,19 @@ fun PlayerRibbon(
) { ) {
Row { Row {
PlayerPortrait( PlayerPortrait(
size = PlayerRibbon.Default.size,
character = it, character = it,
onCharacter = onCharacter, onCharacter = onCharacter,
) )
PlayerPortraitRoll( PlayerPortraitRoll(
modifier = Modifier.size( size = PlayerRibbon.Default.size,
width = 64.dp,
height = PlayerPortrait.Default.size.height
),
value = playerRibbonViewModel.roll(characterId = it.id).value, value = playerRibbonViewModel.roll(characterId = it.id).value,
onRightClick = {
playerRibbonViewModel.onPortraitRollRightClick(characterId = it.characterId)
},
onLeftClick = {
},
) )
} }
} }

View file

@ -2,11 +2,10 @@ package com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.lifecycle.ViewModel 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
@ -38,30 +37,26 @@ class PlayerRibbonViewModel(
initialValue = emptyList() initialValue = emptyList()
) )
private val _rolls: HashMap<String, Int?> = hashMapOf() private val rolls = hashMapOf<String, MutableState<PlayerPortraitRollUio?>>()
val rolls: StateFlow<Map<String, Int?>> = rollHistoryRepository.rolls
.map {
_rolls[it.characterId] = it.rollValue
_rolls
}.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = emptyMap()
)
@Composable @Composable
@Stable @Stable
fun roll(characterId: String): State<Int?> { fun roll(characterId: String): State<PlayerPortraitRollUio?> {
val state = rememberSaveable(characterId) { val state = rolls.getOrPut(characterId) { mutableStateOf(null) }
mutableStateOf<Int?>(null)
}
LaunchedEffect(characterId) { LaunchedEffect(characterId) {
rollHistoryRepository.rolls.collect { rollHistoryRepository.rolls.collect {
if (it.characterId == characterId) { if (it.characterId == characterId) {
state.value = it.rollValue state.value = PlayerPortraitRollUio(
characterId = characterId,
value = it.rollValue,
)
} }
} }
} }
return state return state
} }
fun onPortraitRollRightClick(characterId: String) {
rolls[characterId]?.value = null
}
} }

View file

@ -3,9 +3,8 @@ 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.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.desktop.lwa.repository.characterSheet.SkillDescriptionFactory import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet
import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheet import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CharacteristicId
import com.pixelized.desktop.lwa.repository.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 lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.Res
@ -42,7 +41,6 @@ import org.jetbrains.compose.resources.getString
class CharacterSheetFactory( class CharacterSheetFactory(
private val skillUseCase: ExpressionUseCase, private val skillUseCase: ExpressionUseCase,
private val expressionUseCase: ExpressionUseCase, private val expressionUseCase: ExpressionUseCase,
private val skillDescriptionFactory: SkillDescriptionFactory,
) { ) {
suspend fun convertToUio( suspend fun convertToUio(
@ -213,7 +211,7 @@ class CharacterSheetFactory(
skill = skill, skill = skill,
alterations = alterations[skill.id].sum(), alterations = alterations[skill.id].sum(),
), ),
tooltips = skillDescriptionFactory.baseSkillDescription(id = skill.id)?.let { tooltips = skill.description?.let {
TooltipUio( TooltipUio(
title = skill.label, title = skill.label,
description = it, description = it,

View file

@ -12,7 +12,7 @@ import androidx.lifecycle.viewModelScope
import com.pixelized.desktop.lwa.ui.navigation.screen.destination.CharacterSheetDestination 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.characterSheet.CharacterSheetRepository import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheet import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet
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

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.desktop.lwa.repository.characterSheet.model.CharacterSheet.CharacteristicId import com.pixelized.shared.lwa.model.characterSheet.model.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

@ -3,14 +3,13 @@ package com.pixelized.desktop.lwa.ui.screen.characterSheet.edit
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import com.pixelized.desktop.lwa.business.CharacterSheetUseCase
import com.pixelized.desktop.lwa.repository.characterSheet.SkillDescriptionFactory
import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheet
import com.pixelized.desktop.lwa.ui.screen.characterSheet.edit.common.SkillFieldFactory import com.pixelized.desktop.lwa.ui.screen.characterSheet.edit.common.SkillFieldFactory
import com.pixelized.desktop.lwa.ui.screen.characterSheet.edit.common.occupation 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.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
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__actions__name_label import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__actions__name_label
@ -54,9 +53,8 @@ import java.util.UUID
import kotlin.math.max import kotlin.math.max
class CharacterSheetEditFactory( class CharacterSheetEditFactory(
private val characterSheetUseCase: CharacterSheetUseCase,
private val skillFieldFactory: SkillFieldFactory, private val skillFieldFactory: SkillFieldFactory,
private val skillDescriptionFactory: SkillDescriptionFactory, private val characterSheetUseCase: CharacterSheetUseCase,
) { ) {
suspend fun updateCharacterSheet( suspend fun updateCharacterSheet(
currentSheet: CharacterSheet?, currentSheet: CharacterSheet?,
@ -119,7 +117,7 @@ class CharacterSheetEditFactory(
CharacterSheet.Skill( CharacterSheet.Skill(
id = editedSkill.id, id = editedSkill.id,
label = editedSkill.label, label = editedSkill.label,
description = skillDescriptionFactory.baseSkillDescription(editedSkill.id), 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.takeIf { it.isNotBlank() },

View file

@ -4,8 +4,8 @@ import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.pixelized.desktop.lwa.ui.navigation.screen.destination.CharacterSheetEditDestination
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.desktop.lwa.ui.navigation.screen.destination.CharacterSheetEditDestination
import com.pixelized.desktop.lwa.ui.screen.characterSheet.edit.common.SkillFieldFactory import com.pixelized.desktop.lwa.ui.screen.characterSheet.edit.common.SkillFieldFactory
import com.pixelized.desktop.lwa.ui.screen.characterSheet.edit.composable.ActionFieldUio import com.pixelized.desktop.lwa.ui.screen.characterSheet.edit.composable.ActionFieldUio
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking

View file

@ -4,11 +4,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.lordcodes.turtle.shellRun import com.lordcodes.turtle.shellRun
import com.pixelized.desktop.lwa.repository.OperatingSystem
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.desktop.lwa.repository.network.NetworkRepository import com.pixelized.desktop.lwa.repository.network.NetworkRepository
import com.pixelized.desktop.lwa.repository.storePath
import com.pixelized.desktop.lwa.utils.extention.collectAsState import com.pixelized.desktop.lwa.utils.extention.collectAsState
import com.pixelized.shared.lwa.OperatingSystem
import com.pixelized.shared.lwa.storePath
class MainPageViewModel( class MainPageViewModel(
repository: CharacterSheetRepository, repository: CharacterSheetRepository,

View file

@ -6,13 +6,13 @@ import androidx.compose.animation.core.spring
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.pixelized.desktop.lwa.business.SkillStepUseCase
import com.pixelized.desktop.lwa.business.ExpressionUseCase import com.pixelized.desktop.lwa.business.ExpressionUseCase
import com.pixelized.desktop.lwa.business.SkillStepUseCase
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheet
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 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

@ -5,8 +5,8 @@ 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.characterSheet.CharacterSheetRepository import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheet
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 kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -28,7 +28,8 @@ class RollHistoryViewModel(
add( add(
index = 0, index = 0,
element = RollHistoryItemUio( element = RollHistoryItemUio(
character = sheets.firstOrNull { it.id == content.characterId }?.name ?: "", character = sheets.firstOrNull { it.id == content.characterId }?.name
?: "",
skillLabel = content.skillLabel, skillLabel = content.skillLabel,
rollDifficulty = content.rollDifficulty, rollDifficulty = content.rollDifficulty,
resultLabel = content.resultLabel, resultLabel = content.resultLabel,

View file

@ -20,7 +20,7 @@ val MaterialTheme.lwa: LwaTheme
@Stable @Stable
data class LwaTheme( data class LwaTheme(
val color: LwaColorTheme, val colorScheme: LwaColorTheme,
) )
@Composable @Composable
@ -30,7 +30,7 @@ fun LwaTheme(
val lwaColorTheme = darkLwaColorTheme() val lwaColorTheme = darkLwaColorTheme()
val theme = remember { val theme = remember {
LwaTheme( LwaTheme(
color = lwaColorTheme, colorScheme = lwaColorTheme,
) )
} }

View file

@ -1,6 +1,6 @@
package com.pixelized.desktop.lwa.utils.extention package com.pixelized.desktop.lwa.utils.extention
import com.pixelized.server.lwa.protocol.Message import com.pixelized.shared.lwa.protocol.Message
import io.ktor.websocket.Frame import io.ktor.websocket.Frame
import io.ktor.websocket.readText import io.ktor.websocket.readText
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json

View file

@ -1,14 +1,16 @@
package com.pixelized.desktop.lwa package com.pixelized.desktop.lwa
import androidx.compose.ui.window.application import androidx.compose.ui.window.application
import com.pixelized.shared.lwa.sharedModuleDependencies
import org.koin.compose.KoinContext import org.koin.compose.KoinContext
import org.koin.core.context.loadKoinModules
import org.koin.core.context.startKoin import org.koin.core.context.startKoin
import javax.swing.UIManager import javax.swing.UIManager
fun main() { fun main() {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()) UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName())
startKoin { startKoin {
modules(modules = moduleDependencies) modules(modules = sharedModuleDependencies + appModuleDependencies)
} }
application { application {
KoinContext { KoinContext {

View file

@ -31,15 +31,20 @@ kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-co
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinx-json" } kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinx-json" }
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" } koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
koin-ktor = { module = "io.insert-koin:koin-ktor", version.ref = "koin" }
koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin" } koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin" }
koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel", version.ref = "koin" } koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel", version.ref = "koin" }
ktor-serialization-json = { group = 'io.ktor', name = 'ktor-serialization-kotlinx-json', version.ref = "ktor" }
ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" } ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" }
ktor-client-cio = { group = 'io.ktor', name = "ktor-client-cio", version.ref = "ktor" } ktor-client-cio = { group = 'io.ktor', name = "ktor-client-cio", version.ref = "ktor" }
ktor-client-okhttp = { group = "io.ktor", name = "ktor-client-okhttp", version.ref = "ktor" }
ktor-client-websockets = { group = 'io.ktor', name = "ktor-client-websockets", version.ref = "ktor" } ktor-client-websockets = { group = 'io.ktor', name = "ktor-client-websockets", version.ref = "ktor" }
ktor-client-negotiation = { group = 'io.ktor', name = 'ktor-client-content-negotiation', version.ref = "ktor" }
ktor-server-core = { group = 'io.ktor', name = "ktor-server-core", version.ref = "ktor" } ktor-server-core = { group = 'io.ktor', name = "ktor-server-core", version.ref = "ktor" }
ktor-server-netty = { group = 'io.ktor', name = "ktor-server-netty", version.ref = "ktor" } ktor-server-netty = { group = 'io.ktor', name = "ktor-server-netty", version.ref = "ktor" }
ktor-server-websockets = { group = 'io.ktor', name = "ktor-server-websockets", version.ref = "ktor" } ktor-server-websockets = { group = 'io.ktor', name = "ktor-server-websockets", version.ref = "ktor" }
ktor-server-negotiation = { group = 'io.ktor', name = 'ktor-server-content-negotiation', version.ref = "ktor" }
turtle = { group = "com.lordcodes.turtle", name = "turtle", version.ref = "turtle" } turtle = { group = "com.lordcodes.turtle", name = "turtle", version.ref = "turtle" }
logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }

View file

@ -15,9 +15,11 @@ application {
dependencies { dependencies {
implementation(projects.shared) implementation(projects.shared)
implementation(libs.kotlinx.serialization.json)
implementation(libs.logback) implementation(libs.logback)
implementation(libs.koin.ktor)
implementation(libs.ktor.server.core) implementation(libs.ktor.server.core)
implementation(libs.ktor.server.netty) implementation(libs.ktor.server.netty)
implementation(libs.ktor.server.websockets) implementation(libs.ktor.server.websockets)
implementation(libs.ktor.server.negotiation)
implementation(libs.ktor.serialization.json)
} }

View file

@ -0,0 +1,38 @@
import com.pixelized.server.lwa.model.character.CharacterSheetRepository
import com.pixelized.server.lwa.model.character.CharacterSheetStore
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
val serverModuleDependencies
get() = listOf(
parserDependencies,
factoryDependencies,
useCaseDependencies,
storeDependencies,
repositoryDependencies,
)
val storeDependencies
get() = module {
singleOf(::CharacterSheetStore)
}
val repositoryDependencies
get() = module {
singleOf(::CharacterSheetRepository)
}
val factoryDependencies
get() = module {
}
val parserDependencies
get() = module {
}
val useCaseDependencies
get() = module {
}

View file

@ -1,6 +1,9 @@
package com.pixelized.server.lwa package com.pixelized.server.lwa
import com.pixelized.server.lwa.server.LocalServer import com.pixelized.server.lwa.server.LocalServer
import com.pixelized.shared.lwa.sharedModuleDependencies
import org.koin.core.context.startKoin
import org.koin.java.KoinJavaComponent.inject
fun main() { fun main() {
LocalServer().create().start() LocalServer().create().start()

View file

@ -1,6 +1,6 @@
package com.pixelized.server.lwa.extention package com.pixelized.server.lwa.extention
import com.pixelized.server.lwa.protocol.Message import com.pixelized.shared.lwa.protocol.Message
import io.ktor.websocket.Frame import io.ktor.websocket.Frame
import io.ktor.websocket.readText import io.ktor.websocket.readText
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json

View file

@ -0,0 +1,27 @@
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,119 @@
package com.pixelized.server.lwa.model.character
import com.pixelized.shared.lwa.characterStorePath
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheetJson
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheetJsonFactory
import 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
import java.text.Collator
class CharacterSheetStore(
private val factory: CharacterSheetJsonFactory,
private val jsonFormatter: Json,
) {
private val characterDirectory = File(characterStorePath()).also { it.mkdirs() }
private val flow = MutableStateFlow<List<CharacterSheet>>(value = emptyList())
init {
val scope = CoroutineScope(Dispatchers.IO + Job())
scope.launch {
flow.value = load()
}
}
fun characterSheetFlow(): StateFlow<List<CharacterSheet>> = flow
@Throws(
CharacterSheetStoreException::class,
FileWriteException::class,
JsonConversionException::class,
)
fun save(sheet: CharacterSheet) {
// convert the character sheet into json format.
val json = try {
factory.convertToJson(sheet = sheet).let(jsonFormatter::encodeToString)
} catch (exception: Exception) {
throw JsonConversionException(root = exception)
}
// write the character file.
try {
val file = characterSheetFile(id = sheet.id)
file.writeText(
text = json,
charset = Charsets.UTF_8,
)
} catch (exception: Exception) {
throw FileWriteException(root = exception)
}
// Update the dataflow.
flow.value = flow.value
.toMutableList()
.also { data ->
val index = data.indexOfFirst { it.id == sheet.id }
if (index >= 0) {
data[index] = sheet
} else {
data.add(sheet)
}
}
.sortedWith(compareBy(Collator.getInstance()) { it.name })
}
fun delete(id: String): Boolean {
val file = characterSheetFile(id = id)
flow.value = flow.value.toMutableList()
.also { data ->
data.removeIf { it.id == id }
}
.sortedBy {
it.name
}
return file.delete()
}
@Throws(
CharacterSheetStoreException::class,
FileReadException::class,
JsonConversionException::class,
)
suspend fun load(): List<CharacterSheet> {
return characterDirectory
.listFiles()
?.mapNotNull { file ->
val json = try {
file.readText(charset = Charsets.UTF_8)
} catch (exception: Exception) {
throw FileReadException(root = exception)
}
// Guard, if the json is blank no character have been save, ignore this file.
if (json.isBlank()) {
return@mapNotNull null
}
try {
val sheet = jsonFormatter.decodeFromString<CharacterSheetJson>(json)
factory.convertFromJson(sheet)
} catch (exception: Exception) {
throw JsonConversionException(root = exception)
}
}
?.sortedWith(compareBy(Collator.getInstance()) { it.name })
?: emptyList()
}
private fun characterSheetFile(id: String): File {
return File("${characterStorePath()}${id}.json")
}
sealed class CharacterSheetStoreException(root: Exception) : Exception(root)
class JsonConversionException(root: Exception) : CharacterSheetStoreException(root)
class FileWriteException(root: Exception) : CharacterSheetStoreException(root)
class FileReadException(root: Exception) : CharacterSheetStoreException(root)
}

View file

@ -1,16 +1,25 @@
package com.pixelized.server.lwa.server package com.pixelized.server.lwa.server
import com.pixelized.server.lwa.SERVER_PORT
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.protocol.Message import com.pixelized.server.lwa.model.character.CharacterSheetRepository
import com.pixelized.shared.lwa.SERVER_PORT
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheetJsonFactory
import com.pixelized.shared.lwa.protocol.Message
import com.pixelized.shared.lwa.sharedModuleDependencies
import io.ktor.http.ContentType
import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.install import io.ktor.server.application.install
import io.ktor.server.engine.EmbeddedServer import io.ktor.server.engine.EmbeddedServer
import io.ktor.server.engine.embeddedServer import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty import io.ktor.server.netty.Netty
import io.ktor.server.netty.NettyApplicationEngine import io.ktor.server.netty.NettyApplicationEngine
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
import io.ktor.server.response.respond
import io.ktor.server.response.respondText
import io.ktor.server.routing.get
import io.ktor.server.routing.routing import io.ktor.server.routing.routing
import io.ktor.server.websocket.DefaultWebSocketServerSession
import io.ktor.server.websocket.WebSockets import io.ktor.server.websocket.WebSockets
import io.ktor.server.websocket.pingPeriod import io.ktor.server.websocket.pingPeriod
import io.ktor.server.websocket.timeout import io.ktor.server.websocket.timeout
@ -21,6 +30,9 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import org.koin.ktor.ext.inject
import org.koin.ktor.plugin.Koin
import serverModuleDependencies
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
// https://ktor.io/docs/server-websockets.html#handle-multiple-session // https://ktor.io/docs/server-websockets.html#handle-multiple-session
@ -28,33 +40,78 @@ typealias Server = EmbeddedServer<NettyApplicationEngine, NettyApplicationEngine
class LocalServer { class LocalServer {
private var server: Server? = null private var server: Server? = null
private val json = Json { explicitNulls = true }
private val outgoingMessageBuffer = MutableSharedFlow<Message>() private val outgoingMessageBuffer = MutableSharedFlow<Message>()
fun create(): LocalServer { fun create(
server = build { port: Int = SERVER_PORT, // 16030
val job = launch { ): LocalServer {
// send local message to the clients server = embeddedServer(
outgoingMessageBuffer.collect { message -> factory = Netty,
send(json.encodeToFrame(message)) port = port,
module = {
install(Koin) {
modules(sharedModuleDependencies + serverModuleDependencies)
}
val json by inject<Json>()
install(ContentNegotiation) {
json(json)
}
install(WebSockets) {
pingPeriod = 15.seconds
timeout = 15.seconds
maxFrameSize = Long.MAX_VALUE
masking = false
}
val repository by inject<CharacterSheetRepository>()
val factory by inject<CharacterSheetJsonFactory>()
routing {
get(
path = "/",
body = {
call.respondText(contentType = ContentType.Text.Html) {
"<a href=\"http://127.0.0.1:16030/characters\">characters</a>"
}
}
)
get(
path = "/characters",
body = {
val body = repository.characterSheetFlow().value.map(factory::convertToJson)
call.respond(body)
},
)
webSocket(
path = "/ws",
handler = {
val job = launch {
// send local message to the clients
outgoingMessageBuffer.collect { message ->
send(json.encodeToFrame(message))
}
}
runCatching {
// watching for clients incoming message
incoming.consumeEach { frame ->
if (frame is Frame.Text) {
val message = Json.decodeFromFrame(frame = frame)
println(message)
// broadcast to clients the message
outgoingMessageBuffer.emit(message)
}
}
}.onFailure { exception ->
println("WebSocket exception: ${exception.localizedMessage}")
}.also {
job.cancel()
}
}
)
} }
} }
runCatching { )
// watching for clients incoming message
incoming.consumeEach { frame ->
if (frame is Frame.Text) {
val message = Json.decodeFromFrame(frame = frame)
println(message)
// broadcast to clients the message
outgoingMessageBuffer.emit(message)
}
}
}.onFailure { exception ->
println("WebSocket exception: ${exception.localizedMessage}")
}.also {
job.cancel()
}
}
return this return this
} }
@ -70,28 +127,4 @@ class LocalServer {
} }
} }
} }
private fun build(
port: Int = SERVER_PORT,
handler: suspend DefaultWebSocketServerSession.() -> Unit,
): EmbeddedServer<NettyApplicationEngine, NettyApplicationEngine.Configuration> {
return embeddedServer(
factory = Netty,
port = port,
module = {
install(WebSockets) {
pingPeriod = 15.seconds
timeout = 15.seconds
maxFrameSize = Long.MAX_VALUE
masking = false
}
routing {
webSocket(
path = "/ws",
handler = handler,
)
}
},
)
}
} }

View file

@ -8,6 +8,7 @@ kotlin {
sourceSets { sourceSets {
commonMain.dependencies { commonMain.dependencies {
implementation(libs.koin.core)
implementation(libs.kotlinx.serialization.json) implementation(libs.kotlinx.serialization.json)
} }
} }

View file

@ -1,3 +0,0 @@
package com.pixelized.server.lwa
const val SERVER_PORT = 16030

View file

@ -1,5 +0,0 @@
package com.pixelized.server.lwa.protocol
enum class MessageType {
Roll
}

View file

@ -0,0 +1,3 @@
package com.pixelized.shared.lwa
const val SERVER_PORT = 16030

View file

@ -0,0 +1,34 @@
package com.pixelized.shared.lwa
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheetJsonFactory
import com.pixelized.shared.lwa.usecase.CharacterSheetUseCase
import kotlinx.serialization.json.Json
import org.koin.core.module.dsl.factoryOf
import org.koin.dsl.module
val sharedModuleDependencies
get() = listOf(
toolsDependencies,
factoryDependencies,
useCaseDependencies,
)
val toolsDependencies
get() = module {
factory {
Json {
explicitNulls = false
prettyPrint = true
}
}
}
val factoryDependencies
get() = module {
factoryOf(::CharacterSheetJsonFactory)
}
val useCaseDependencies
get() = module {
factoryOf(::CharacterSheetUseCase)
}

View file

@ -1,4 +1,4 @@
package com.pixelized.desktop.lwa.repository package com.pixelized.shared.lwa
enum class OperatingSystem( enum class OperatingSystem(
val home: String = System.getProperty("user.home"), val home: String = System.getProperty("user.home"),

View file

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

View file

@ -0,0 +1,11 @@
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

@ -0,0 +1,39 @@
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

@ -0,0 +1,6 @@
package com.pixelized.shared.lwa.model.campaign.model
import kotlinx.serialization.Serializable
@Serializable
sealed interface CampaignJson

View file

@ -0,0 +1,15 @@
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,4 +1,4 @@
package com.pixelized.desktop.lwa.repository.characterSheet.model package com.pixelized.shared.lwa.model.characterSheet.model
data class CharacterSheet( data class CharacterSheet(
val id: String, val id: String,

View file

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

View file

@ -1,14 +1,101 @@
package com.pixelized.desktop.lwa.repository.characterSheet package com.pixelized.shared.lwa.model.characterSheet.model
import com.pixelized.shared.lwa.usecase.CharacterSheetUseCase
import com.pixelized.desktop.lwa.business.CharacterSheetUseCase
import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheet
import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheetJson
import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheetJsonV1
class CharacterSheetJsonFactory( class CharacterSheetJsonFactory(
private val skillDescriptionFactory: SkillDescriptionFactory,
private val characterSheetUseCase: CharacterSheetUseCase, private val characterSheetUseCase: CharacterSheetUseCase,
) { ) {
suspend fun convertFromJson(
json: CharacterSheetJson,
): CharacterSheet {
return when (json) {
is CharacterSheetJsonV1 -> convertFromV1(json = json)
}
}
private suspend fun convertFromV1(
json: CharacterSheetJsonV1,
): CharacterSheet = characterSheetUseCase.run {
CharacterSheet(
id = json.id,
name = json.name,
portrait = json.portrait,
thumbnail = json.thumbnail,
strength = json.strength,
dexterity = json.dexterity,
constitution = json.constitution,
height = json.height,
intelligence = json.intelligence,
power = json.power,
charisma = json.charisma,
overrideMovement = json.movement != null,
movement = json.movement ?: defaultMovement(),
currentHp = json.currentHp,
overrideMaxHp = json.maxHp != null,
maxHp = json.maxHp ?: defaultMaxHp(
constitution = json.constitution,
height = json.height,
),
currentPp = json.currentPP,
overrideMaxPP = json.maxPP != null,
maxPp = json.maxPP ?: defaultMaxPower(power = json.power),
overrideDamageBonus = json.damageBonus != null,
damageBonus = json.damageBonus ?: defaultDamageBonus(
strength = json.strength,
height = json.height,
),
overrideArmor = json.armor != null,
armor = json.armor ?: defaultArmor(),
overrideLearning = json.learning != null,
learning = json.learning ?: defaultLearning(intelligence = json.intelligence),
overrideHpGrow = json.hpGrowf != null,
hpGrow = json.hpGrowf ?: defaultHpGrow(constitution = json.constitution),
commonSkills = json.skills.map {
CharacterSheet.Skill(
id = it.id,
label = it.label,
description = it.description,
base = it.base,
bonus = it.bonus,
level = it.level,
occupation = it.occupation,
used = it.used,
)
},
specialSkills = json.occupations.map {
CharacterSheet.Skill(
id = it.id,
label = it.label,
description = it.description,
base = it.base,
bonus = it.bonus,
level = it.level,
occupation = it.occupation,
used = it.used,
)
},
magicSkills = json.magics.map {
CharacterSheet.Skill(
id = it.id,
label = it.label,
description = it.description,
base = it.base,
bonus = it.bonus,
level = it.level,
occupation = it.occupation,
used = it.used,
)
},
actions = json.rolls.map {
CharacterSheet.Roll(
id = it.id,
label = it.label,
roll = it.roll,
)
},
)
}
fun convertToJson( fun convertToJson(
sheet: CharacterSheet, sheet: CharacterSheet,
@ -80,95 +167,4 @@ class CharacterSheetJsonFactory(
) )
return json return json
} }
suspend fun convertFromJson(
json: CharacterSheetJson,
): CharacterSheet {
return when (json) {
is CharacterSheetJsonV1 -> convertFromV1(json = json)
}
}
private suspend fun convertFromV1(
json: CharacterSheetJsonV1,
): CharacterSheet = characterSheetUseCase.run {
CharacterSheet(
id = json.id,
name = json.name,
portrait = json.portrait,
thumbnail = json.thumbnail,
strength = json.strength,
dexterity = json.dexterity,
constitution = json.constitution,
height = json.height,
intelligence = json.intelligence,
power = json.power,
charisma = json.charisma,
overrideMovement = json.movement != null,
movement = json.movement ?: defaultMovement(),
currentHp = json.currentHp,
overrideMaxHp = json.maxHp != null,
maxHp = json.maxHp ?: defaultMaxHp(
constitution = json.constitution,
height = json.height,
),
currentPp = json.currentPP,
overrideMaxPP = json.maxPP != null,
maxPp = json.maxPP ?: defaultMaxPower(power = json.power),
overrideDamageBonus = json.damageBonus != null,
damageBonus = json.damageBonus ?: defaultDamageBonus(
strength = json.strength,
height = json.height,
),
overrideArmor = json.armor != null,
armor = json.armor ?: defaultArmor(),
overrideLearning = json.learning != null,
learning = json.learning ?: defaultLearning(intelligence = json.intelligence),
overrideHpGrow = json.hpGrowf != null,
hpGrow = json.hpGrowf ?: defaultHpGrow(constitution = json.constitution),
commonSkills = json.skills.map {
CharacterSheet.Skill(
id = it.id,
label = it.label,
description = skillDescriptionFactory.baseSkillDescription(id = json.id),
base = it.base,
bonus = it.bonus,
level = it.level,
occupation = it.occupation,
used = it.used,
)
},
specialSkills = json.occupations.map {
CharacterSheet.Skill(
id = it.id,
label = it.label,
description = it.description,
base = it.base,
bonus = it.bonus,
level = it.level,
occupation = it.occupation,
used = it.used,
)
},
magicSkills = json.magics.map {
CharacterSheet.Skill(
id = it.id,
label = it.label,
description = it.description,
base = it.base,
bonus = it.bonus,
level = it.level,
occupation = it.occupation,
used = it.used,
)
},
actions = json.rolls.map {
CharacterSheet.Roll(
id = it.id,
label = it.label,
roll = it.roll,
)
},
)
}
} }

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
package com.pixelized.server.lwa.protocol.roll package com.pixelized.shared.lwa.protocol.roll
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable

View file

@ -1,4 +1,4 @@
package com.pixelized.desktop.lwa.business package com.pixelized.shared.lwa.usecase
import kotlin.math.ceil import kotlin.math.ceil
import kotlin.math.max import kotlin.math.max