Refactor project data to allow server handling.
This commit is contained in:
parent
3c8eecdab5
commit
1e5f0d88ae
58 changed files with 742 additions and 469 deletions
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.** { *; }
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -83,9 +83,7 @@ fun CampaignScreen(
|
|||
},
|
||||
leftOverlay = {
|
||||
PlayerRibbon(
|
||||
modifier = Modifier
|
||||
.padding(all = 8.dp)
|
||||
.fillMaxHeight(),
|
||||
modifier = Modifier.fillMaxHeight(),
|
||||
onCharacter = {
|
||||
characterDetailViewModel.showCharacter(id = it)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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() },
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
38
server/src/main/kotlin/Module.kt
Normal file
38
server/src/main/kotlin/Module.kt
Normal 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 {
|
||||
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ kotlin {
|
|||
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
implementation(libs.koin.core)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
package com.pixelized.server.lwa
|
||||
|
||||
const val SERVER_PORT = 16030
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
package com.pixelized.server.lwa.protocol
|
||||
|
||||
enum class MessageType {
|
||||
Roll
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package com.pixelized.shared.lwa
|
||||
|
||||
const val SERVER_PORT = 16030
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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"),
|
||||
|
|
@ -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,
|
||||
) {
|
||||
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.pixelized.shared.lwa.model.campaign.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
sealed interface CampaignJson
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.pixelized.desktop.lwa.repository.characterSheet.model
|
||||
package com.pixelized.shared.lwa.model.characterSheet.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
|
|
@ -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,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.pixelized.desktop.lwa.repository.characterSheet.model
|
||||
package com.pixelized.shared.lwa.model.characterSheet.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.pixelized.server.lwa.protocol
|
||||
package com.pixelized.shared.lwa.protocol
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.pixelized.shared.lwa.protocol
|
||||
|
||||
enum class MessageType {
|
||||
Roll
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.pixelized.server.lwa.protocol.roll
|
||||
package com.pixelized.shared.lwa.protocol.roll
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.pixelized.desktop.lwa.business
|
||||
package com.pixelized.shared.lwa.usecase
|
||||
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.max
|
||||
Loading…
Add table
Add a link
Reference in a new issue