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
commonMain.dependencies {
// common
implementation(projects.shared)
// compose
implementation(compose.runtime)
implementation(compose.foundation)
@ -26,19 +28,18 @@ kotlin {
implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.androidx.navigation.compose)
// injection
api(libs.koin.core)
implementation(libs.koin.compose)
implementation(libs.koin.compose.viewmodel)
// composable component.
implementation(libs.coil.compose)
implementation(libs.coil.network)
// common
implementation(projects.shared)
// network
implementation(libs.kotlinx.serialization.json)
implementation(libs.ktor.serialization.json)
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.cio)
implementation(libs.ktor.client.okhttp)
implementation(libs.ktor.client.websockets)
implementation(libs.ktor.client.negotiation)
// shell
implementation(libs.turtle)
}

View file

@ -10,4 +10,8 @@
-keep class kotlinx.coroutines.** { *; }
# 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.screen.campaign.player.CampaignScreen
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.theme.LwaTheme
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.ui.tooling.preview.Preview
import org.koin.compose.koinInject
import org.koin.compose.viewmodel.koinViewModel
val LocalWindowController = compositionLocalOf<WindowController> {
error("Local Window Controller is not yet ready")
@ -125,8 +127,8 @@ fun ApplicationScope.App() {
}
},
content = {
// MainNavHost()
CampaignScreen()
MainNavHost()
// CampaignScreen()
}
)
NetworkSnackHandler(

View file

@ -1,21 +1,24 @@
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.SettingsUseCase
import com.pixelized.desktop.lwa.business.SkillStepUseCase
import com.pixelized.desktop.lwa.business.ExpressionUseCase
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetJsonFactory
import com.pixelized.desktop.lwa.parser.dice.DiceParser
import com.pixelized.desktop.lwa.parser.expression.ExpressionParser
import com.pixelized.desktop.lwa.parser.word.WordParser
import com.pixelized.desktop.lwa.repository.alteration.AlterationRepository
import com.pixelized.desktop.lwa.repository.alteration.AlterationStore
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
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.roll_history.RollHistoryRepository
import com.pixelized.desktop.lwa.repository.settings.SettingsFactory
import com.pixelized.desktop.lwa.repository.settings.SettingsRepository
import com.pixelized.desktop.lwa.repository.alteration.AlterationStore
import com.pixelized.desktop.lwa.repository.alteration.AlterationRepository
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.CharacterSheetViewModel
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.roll.RollViewModel
import com.pixelized.desktop.lwa.ui.screen.rollhistory.RollHistoryViewModel
import com.pixelized.desktop.lwa.parser.dice.DiceParser
import com.pixelized.desktop.lwa.parser.word.WordParser
import com.pixelized.desktop.lwa.parser.expression.ExpressionParser
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 kotlinx.serialization.json.Json
import com.pixelized.shared.lwa.model.campaign.CampaignRepository
import com.pixelized.shared.lwa.model.campaign.model.CampaignFactory
import com.pixelized.shared.lwa.usecase.CharacterSheetUseCase
import io.ktor.client.HttpClient
import io.ktor.client.engine.HttpClientEngine
import io.ktor.client.engine.okhttp.OkHttp
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.singleOf
import org.koin.core.module.dsl.viewModelOf
import org.koin.dsl.module
val moduleDependencies
val appModuleDependencies
get() = listOf(
toolsDependencies,
parserDependencies,
factoryDependencies,
useCaseDependencies,
storeDependencies,
repositoryDependencies,
viewModelDependencies,
toolsDependencies,
)
val toolsDependencies
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
@ -68,17 +87,17 @@ val repositoryDependencies
singleOf(::RollHistoryRepository)
singleOf(::SettingsRepository)
singleOf(::AlterationRepository)
singleOf(::CampaignRepository)
}
val factoryDependencies
get() = module {
factoryOf(::CharacterSheetFactory)
factoryOf(::CharacterSheetEditFactory)
factoryOf(::CharacterSheetJsonFactory)
factoryOf(::NetworkFactory)
factoryOf(::SkillFieldFactory)
factoryOf(::SkillDescriptionFactory)
factoryOf(::SettingsFactory)
factoryOf(::CampaignFactory)
}
val viewModelDependencies
@ -106,6 +125,6 @@ val useCaseDependencies
factoryOf(::SkillStepUseCase)
factoryOf(::RollUseCase)
factoryOf(::ExpressionUseCase)
factoryOf(::CharacterSheetUseCase)
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.ExpressionParser
import com.pixelized.desktop.lwa.parser.word.Word
import com.pixelized.desktop.lwa.repository.alteration.model.FieldAlteration
import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheet
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet
import kotlin.math.max
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.repository.alteration.model.Alteration
import com.pixelized.desktop.lwa.repository.alteration.model.AlterationMetadata
import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheet.CharacteristicId
import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheet.CharacteristicId.ARMOR
import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheet.CharacteristicId.DEX
import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheet.CharacteristicId.HEI
import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheet.CharacteristicId.MOV
import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheet.CharacteristicId.STR
import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheet.CommonSkillId.ACROBATICS_ID
import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheet.CommonSkillId.AID_ID
import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheet.CommonSkillId.ATHLETICS_ID
import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheet.CommonSkillId.BARGAIN_ID
import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheet.CommonSkillId.COMBAT_ID
import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheet.CommonSkillId.DISCRETION_ID
import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheet.CommonSkillId.INTIMIDATION_ID
import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheet.CommonSkillId.PERCEPTION_ID
import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheet.CommonSkillId.PERSUASION_ID
import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheet.CommonSkillId.SLEIGHT_OF_HAND_ID
import com.pixelized.desktop.lwa.repository.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.CharacteristicId
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CharacteristicId.ARMOR
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CharacteristicId.DEX
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CharacteristicId.HEI
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CharacteristicId.MOV
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CharacteristicId.STR
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CommonSkillId.ACROBATICS_ID
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CommonSkillId.AID_ID
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CommonSkillId.ATHLETICS_ID
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CommonSkillId.BARGAIN_ID
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CommonSkillId.COMBAT_ID
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CommonSkillId.DISCRETION_ID
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CommonSkillId.INTIMIDATION_ID
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CommonSkillId.PERCEPTION_ID
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CommonSkillId.PERSUASION_ID
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CommonSkillId.SLEIGHT_OF_HAND_ID
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CommonSkillId.SPIEL_ID
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CommonSkillId.THROW_ID
class AlterationStore(
private val expressionParser: ExpressionParser,

View file

@ -1,6 +1,6 @@
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.Dispatchers
import kotlinx.coroutines.Job
@ -53,11 +53,11 @@ class CharacterSheetRepository(
}
fun save(characterSheet: CharacterSheet) {
store.save(sheet = characterSheet)
// store.save(sheet = characterSheet)
}
fun delete(id: String) {
store.delete(id = id)
// store.delete(id = id)
}
fun setDiminishedForCharacter(id: String, diminished: Int) {
@ -65,5 +65,4 @@ class CharacterSheetRepository(
this[id] = diminished
}
}
}
}

View file

@ -1,24 +1,25 @@
package com.pixelized.desktop.lwa.repository.characterSheet
import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheet
import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheetJson
import com.pixelized.desktop.lwa.repository.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 io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.request.get
import io.ktor.serialization.kotlinx.json.json
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.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 client: HttpClient,
) {
private val characterDirectory = File(characterStorePath()).also { it.mkdirs() }
private val flow = MutableStateFlow<List<CharacterSheet>>(value = emptyList())
init {
@ -30,85 +31,22 @@ class CharacterSheetStore(
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()
return false
}
@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")
val request: List<CharacterSheetJson> = client
.get("http://pixelized.freeboxos.fr:16030/characters")
.body()
val data = request.map {
factory.convertFromJson(json = it)
}
return data
}
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
//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.settings.SettingsRepository
import com.pixelized.desktop.lwa.utils.extention.decodeFromFrame
import com.pixelized.desktop.lwa.utils.extention.encodeToFrame
import com.pixelized.server.lwa.SERVER_PORT
import com.pixelized.server.lwa.protocol.Message
import com.pixelized.server.lwa.protocol.MessageType
import com.pixelized.shared.lwa.SERVER_PORT
import com.pixelized.shared.lwa.protocol.Message
import com.pixelized.shared.lwa.protocol.MessageType
import io.ktor.client.HttpClient
import io.ktor.websocket.Frame
import kotlinx.coroutines.CoroutineScope
@ -25,10 +21,9 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json
typealias Client = HttpClient
class NetworkRepository(
private val settingsRepository: SettingsRepository,
private val client: HttpClient,
) {
companion object {
const val DEFAULT_PORT = SERVER_PORT
@ -37,7 +32,6 @@ class NetworkRepository(
private val scope = CoroutineScope(Dispatchers.IO)
private var networkJob: Job? = null
private var client: Client? = null
private val outgoingMessageBuffer = MutableSharedFlow<Message>()
private val incomingMessageBuffer = MutableSharedFlow<Message>()
@ -46,7 +40,6 @@ class NetworkRepository(
private val _status = MutableStateFlow(Status.DISCONNECTED)
val status: StateFlow<Status> get() = _status
fun connect(
host: String,
port: Int,
@ -54,12 +47,10 @@ class NetworkRepository(
onFailure: (Exception) -> Unit = { },
onClose: () -> Unit = { },
) {
client = client()
networkJob?.cancel()
networkJob = scope.launch {
try {
client?.connectWebSocket(host = host, port = port) {
client.connectWebSocket(host = host, port = port) {
_status.value = Status.CONNECTED
onConnect()
@ -93,7 +84,7 @@ class NetworkRepository(
fun disconnect() {
networkJob?.cancel()
scope.launch {
client?.close()
client.close()
}
}

View file

@ -1,26 +1,15 @@
package com.pixelized.desktop.lwa.repository.network.helper
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.WebSockets
import io.ktor.client.plugins.websocket.webSocket
import io.ktor.http.HttpMethod
// 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(
host: String,
port: Int,
block: suspend DefaultClientWebSocketSession.() -> Unit
block: suspend DefaultClientWebSocketSession.() -> Unit,
) {
webSocket(
method = HttpMethod.Get,

View file

@ -1,8 +1,8 @@
package com.pixelized.desktop.lwa.repository.roll_history
import com.pixelized.desktop.lwa.repository.network.NetworkRepository
import com.pixelized.server.lwa.protocol.MessageType
import com.pixelized.server.lwa.protocol.roll.RollMessage
import com.pixelized.shared.lwa.protocol.MessageType
import com.pixelized.shared.lwa.protocol.roll.RollMessage
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
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.repository.settings.model.Settings
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.Dispatchers
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.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.compositionLocalOf
import androidx.navigation.NavHostController
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.composableMainPage
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> {
error("MainNavHost controller is not yet ready")
@ -18,7 +21,12 @@ val LocalScreenController = compositionLocalOf<NavHostController> {
fun MainNavHost(
controller: NavHostController = rememberNavController(),
startDestination: String = MainDestination.navigationRoute(),
networkViewModel: NetworkViewModel = koinViewModel(),
) {
LaunchedEffect(Unit) {
networkViewModel.connect()
}
CompositionLocalProvider(
LocalScreenController provides controller,
) {

View file

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

View file

@ -141,7 +141,7 @@ private fun Background(
) {
Surface(
modifier = modifier.fillMaxSize(),
color = MaterialTheme.lwa.color.elevatedSurface,
color = MaterialTheme.lwa.colorScheme.elevatedSurface,
) {
// Image(
// modifier = Modifier.fillMaxSize().drawWithContent {
@ -200,7 +200,7 @@ private fun CharacterHeader(
) {
Icon(
painter = painterResource(Res.drawable.ic_close_24dp),
tint = MaterialTheme.lwa.color.base.primary,
tint = MaterialTheme.lwa.colorScheme.base.primary,
contentDescription = null,
)
}
@ -220,7 +220,7 @@ private fun CharacterHeader(
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.h6,
color = MaterialTheme.lwa.color.base.primary,
color = MaterialTheme.lwa.colorScheme.base.primary,
fontWeight = FontWeight.Bold,
text = dynDetail.value.hp,
)
@ -243,7 +243,7 @@ private fun CharacterHeader(
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.h6,
color = MaterialTheme.lwa.color.base.primary,
color = MaterialTheme.lwa.colorScheme.base.primary,
fontWeight = FontWeight.Bold,
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.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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.drawWithContent
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
import com.pixelized.desktop.lwa.ui.composable.decoratedBox.DecoratedBox
import com.pixelized.desktop.lwa.ui.theme.lwa
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.ic_heart_24dp
import lwacharactersheet.composeapp.generated.resources.ic_water_drop_24dp
@ -42,19 +41,15 @@ data class PlayerPortraitUio(
val maxPp: Int,
)
object PlayerPortrait {
object Default {
val size = DpSize(96.dp, 128.dp)
}
}
@Composable
fun PlayerPortrait(
modifier: Modifier = Modifier,
size: DpSize = PlayerPortrait.Default.size,
size: DpSize,
character: PlayerPortraitUio,
onCharacter: (id: String) -> Unit,
) {
val colorScheme = MaterialTheme.lwa.colorScheme
DecoratedBox(
modifier = modifier
.size(size = size)
@ -76,11 +71,11 @@ fun PlayerPortrait(
drawRect(
brush = Brush.verticalGradient(
listOf(
Color.Black.copy(alpha = 0.0f),
Color.Black.copy(alpha = 0.0f),
Color.Black.copy(alpha = 0.0f),
Color.Black.copy(alpha = 0.5f),
Color.Black.copy(alpha = 0.8f),
colorScheme.elevatedSurface.copy(alpha = 0.0f),
colorScheme.elevatedSurface.copy(alpha = 0.0f),
colorScheme.elevatedSurface.copy(alpha = 0.0f),
colorScheme.elevatedSurface.copy(alpha = 0.5f),
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.fadeOut
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.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
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.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Shadow
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.unit.DpSize
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.ic_d20_24dp
import org.jetbrains.compose.resources.painterResource
@Stable
data class PlayerPortraitRollUio(
val characterId: String,
val value: Int?,
)
@Stable
data class PlayerPortraitRollAnimation(
val alpha: Animatable<Float, AnimationVector1D> = Animatable(0f),
@ -38,13 +55,19 @@ data class PlayerPortraitRollAnimation(
val scale: Animatable<Float, AnimationVector1D> = Animatable(1f),
)
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun PlayerPortraitRoll(
modifier: Modifier = Modifier,
value: Int?,
size: DpSize,
value: PlayerPortraitRollUio?,
onLeftClick: (PlayerPortraitRollUio) -> Unit,
onRightClick: (PlayerPortraitRollUio) -> Unit,
) {
AnimatedContent(
modifier = modifier.graphicsLayer { clip = false },
modifier = modifier
.size(size = size)
.graphicsLayer { clip = false },
targetState = value,
transitionSpec = {
val enter = fadeIn()
@ -61,33 +84,41 @@ fun PlayerPortraitRoll(
},
contentAlignment = Alignment.Center,
) {
when (it) {
null -> Unit
else -> {
Icon(
modifier = Modifier
.graphicsLayer {
this.alpha = 0.8f
this.rotationZ = animation.rotation.value
}
.size(48.dp),
painter = painterResource(Res.drawable.ic_d20_24dp),
tint = MaterialTheme.colors.primary,
contentDescription = null,
)
Text(
style = MaterialTheme.typography.h5.copy(
shadow = Shadow(
color = MaterialTheme.colors.surface,
offset = Offset.Zero,
blurRadius = 8f,
)
),
textAlign = TextAlign.Center,
color = MaterialTheme.colors.onSurface,
text = it.toString()
)
}
if (it != null) {
Icon(
modifier = Modifier
.graphicsLayer {
this.alpha = 0.8f
this.rotationZ = animation.rotation.value
}
.fillMaxWidth()
.aspectRatio(1f)
.padding(all = 8.dp)
.clip(shape = CircleShape)
.onClick(
matcher = PointerMatcher.mouse(PointerButton.Secondary),
onClick = { onRightClick(it) },
).clickable {
onLeftClick(it)
}
.padding(all = 8.dp),
painter = painterResource(Res.drawable.ic_d20_24dp),
tint = MaterialTheme.colors.primary,
contentDescription = null,
)
Text(
style = MaterialTheme.typography.h5.copy(
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
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.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import org.koin.compose.viewmodel.koinViewModel
object PlayerRibbon {
object Default {
val size = DpSize(96.dp, 128.dp)
}
}
@Composable
fun PlayerRibbon(
modifier: Modifier = Modifier,
playerRibbonViewModel: PlayerRibbonViewModel = koinViewModel(),
padding: PaddingValues = PaddingValues(all = 8.dp),
onCharacter: (id: String) -> Unit,
) {
val characters = playerRibbonViewModel.characters.collectAsState()
LazyColumn(
modifier = modifier,
contentPadding = padding,
verticalArrangement = Arrangement.spacedBy(space = 8.dp)
) {
items(
@ -29,15 +39,19 @@ fun PlayerRibbon(
) {
Row {
PlayerPortrait(
size = PlayerRibbon.Default.size,
character = it,
onCharacter = onCharacter,
)
PlayerPortraitRoll(
modifier = Modifier.size(
width = 64.dp,
height = PlayerPortrait.Default.size.height
),
size = PlayerRibbon.Default.size,
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.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
@ -38,30 +37,26 @@ class PlayerRibbonViewModel(
initialValue = emptyList()
)
private val _rolls: HashMap<String, Int?> = hashMapOf()
val rolls: StateFlow<Map<String, Int?>> = rollHistoryRepository.rolls
.map {
_rolls[it.characterId] = it.rollValue
_rolls
}.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = emptyMap()
)
private val rolls = hashMapOf<String, MutableState<PlayerPortraitRollUio?>>()
@Composable
@Stable
fun roll(characterId: String): State<Int?> {
val state = rememberSaveable(characterId) {
mutableStateOf<Int?>(null)
}
fun roll(characterId: String): State<PlayerPortraitRollUio?> {
val state = rolls.getOrPut(characterId) { mutableStateOf(null) }
LaunchedEffect(characterId) {
rollHistoryRepository.rolls.collect {
if (it.characterId == characterId) {
state.value = it.rollValue
state.value = PlayerPortraitRollUio(
characterId = characterId,
value = it.rollValue,
)
}
}
}
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.ui.composable.tooltip.TooltipUio
import com.pixelized.desktop.lwa.repository.alteration.model.FieldAlteration
import com.pixelized.desktop.lwa.repository.characterSheet.SkillDescriptionFactory
import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheet
import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheet.CharacteristicId
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet.CharacteristicId
import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.CharacterSheetPageUio.Characteristic
import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.CharacterSheetPageUio.Node
import lwacharactersheet.composeapp.generated.resources.Res
@ -42,7 +41,6 @@ import org.jetbrains.compose.resources.getString
class CharacterSheetFactory(
private val skillUseCase: ExpressionUseCase,
private val expressionUseCase: ExpressionUseCase,
private val skillDescriptionFactory: SkillDescriptionFactory,
) {
suspend fun convertToUio(
@ -213,7 +211,7 @@ class CharacterSheetFactory(
skill = skill,
alterations = alterations[skill.id].sum(),
),
tooltips = skillDescriptionFactory.baseSkillDescription(id = skill.id)?.let {
tooltips = skill.description?.let {
TooltipUio(
title = skill.label,
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.repository.alteration.AlterationRepository
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.DiminishedStatDialogUio
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.Stable
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.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.derivedStateOf
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.occupation
import com.pixelized.desktop.lwa.ui.screen.characterSheet.edit.composable.ActionFieldUio
import com.pixelized.desktop.lwa.ui.screen.characterSheet.edit.composable.BaseSkillFieldUio
import com.pixelized.desktop.lwa.ui.screen.characterSheet.edit.composable.SimpleFieldUio
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet
import com.pixelized.shared.lwa.usecase.CharacterSheetUseCase
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__name_label
@ -54,9 +53,8 @@ import java.util.UUID
import kotlin.math.max
class CharacterSheetEditFactory(
private val characterSheetUseCase: CharacterSheetUseCase,
private val skillFieldFactory: SkillFieldFactory,
private val skillDescriptionFactory: SkillDescriptionFactory,
private val characterSheetUseCase: CharacterSheetUseCase,
) {
suspend fun updateCharacterSheet(
currentSheet: CharacterSheet?,
@ -119,7 +117,7 @@ class CharacterSheetEditFactory(
CharacterSheet.Skill(
id = editedSkill.id,
label = editedSkill.label,
description = skillDescriptionFactory.baseSkillDescription(editedSkill.id),
description = currentSkill?.description,
base = "${editedSkill.base.value}",
bonus = editedSkill.bonus.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.lifecycle.SavedStateHandle
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.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.composable.ActionFieldUio
import kotlinx.coroutines.runBlocking

View file

@ -4,11 +4,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.lifecycle.ViewModel
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.network.NetworkRepository
import com.pixelized.desktop.lwa.repository.storePath
import com.pixelized.desktop.lwa.utils.extention.collectAsState
import com.pixelized.shared.lwa.OperatingSystem
import com.pixelized.shared.lwa.storePath
class MainPageViewModel(
repository: CharacterSheetRepository,

View file

@ -6,13 +6,13 @@ import androidx.compose.animation.core.spring
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import com.pixelized.desktop.lwa.business.SkillStepUseCase
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.model.CharacterSheet
import com.pixelized.desktop.lwa.repository.roll_history.RollHistoryRepository
import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.CharacterSheetPageUio
import com.pixelized.desktop.lwa.ui.screen.roll.DifficultyUio.Difficulty
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet
import kotlinx.coroutines.Job
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay

View file

@ -5,8 +5,8 @@ import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
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.shared.lwa.model.characterSheet.model.CharacterSheet
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
@ -28,7 +28,8 @@ class RollHistoryViewModel(
add(
index = 0,
element = RollHistoryItemUio(
character = sheets.firstOrNull { it.id == content.characterId }?.name ?: "",
character = sheets.firstOrNull { it.id == content.characterId }?.name
?: "",
skillLabel = content.skillLabel,
rollDifficulty = content.rollDifficulty,
resultLabel = content.resultLabel,

View file

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

View file

@ -1,6 +1,6 @@
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.readText
import kotlinx.serialization.json.Json

View file

@ -1,14 +1,16 @@
package com.pixelized.desktop.lwa
import androidx.compose.ui.window.application
import com.pixelized.shared.lwa.sharedModuleDependencies
import org.koin.compose.KoinContext
import org.koin.core.context.loadKoinModules
import org.koin.core.context.startKoin
import javax.swing.UIManager
fun main() {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName())
startKoin {
modules(modules = moduleDependencies)
modules(modules = sharedModuleDependencies + appModuleDependencies)
}
application {
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" }
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-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-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-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-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-negotiation = { group = 'io.ktor', name = 'ktor-server-content-negotiation', version.ref = "ktor" }
turtle = { group = "com.lordcodes.turtle", name = "turtle", version.ref = "turtle" }
logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }

View file

@ -15,9 +15,11 @@ application {
dependencies {
implementation(projects.shared)
implementation(libs.kotlinx.serialization.json)
implementation(libs.logback)
implementation(libs.koin.ktor)
implementation(libs.ktor.server.core)
implementation(libs.ktor.server.netty)
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
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() {
LocalServer().create().start()

View file

@ -1,6 +1,6 @@
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.readText
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
import com.pixelized.server.lwa.SERVER_PORT
import com.pixelized.server.lwa.extention.decodeFromFrame
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.engine.EmbeddedServer
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
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.websocket.DefaultWebSocketServerSession
import io.ktor.server.websocket.WebSockets
import io.ktor.server.websocket.pingPeriod
import io.ktor.server.websocket.timeout
@ -21,6 +30,9 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
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
// https://ktor.io/docs/server-websockets.html#handle-multiple-session
@ -28,33 +40,78 @@ typealias Server = EmbeddedServer<NettyApplicationEngine, NettyApplicationEngine
class LocalServer {
private var server: Server? = null
private val json = Json { explicitNulls = true }
private val outgoingMessageBuffer = MutableSharedFlow<Message>()
fun create(): LocalServer {
server = build {
val job = launch {
// send local message to the clients
outgoingMessageBuffer.collect { message ->
send(json.encodeToFrame(message))
fun create(
port: Int = SERVER_PORT, // 16030
): LocalServer {
server = embeddedServer(
factory = Netty,
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
}
@ -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 {
commonMain.dependencies {
implementation(libs.koin.core)
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(
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(
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

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(
private val skillDescriptionFactory: SkillDescriptionFactory,
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(
sheet: CharacterSheet,
@ -80,95 +167,4 @@ class CharacterSheetJsonFactory(
)
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

View file

@ -1,4 +1,4 @@
package com.pixelized.server.lwa.protocol
package com.pixelized.shared.lwa.protocol
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

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.max