Add campagin + character REST API.

This commit is contained in:
Thomas Andres Gomez 2025-02-24 11:16:21 +01:00
parent 495768e5fe
commit bd4d65fe6a
34 changed files with 592 additions and 162 deletions

View file

@ -3,6 +3,7 @@ import com.pixelized.server.lwa.model.campaign.CampaignStore
import com.pixelized.server.lwa.model.character.CharacterSheetService
import com.pixelized.server.lwa.model.character.CharacterSheetStore
import com.pixelized.server.lwa.server.Engine
import org.koin.core.module.dsl.createdAtStart
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
@ -18,7 +19,7 @@ val serverModuleDependencies
val engineDependencies
get() = module {
singleOf(::Engine)
singleOf(constructor = ::Engine, options = { createdAtStart() })
}
val storeDependencies

View file

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

View file

@ -3,12 +3,14 @@ package com.pixelized.server.lwa.model.campaign
import com.pixelized.shared.lwa.model.campaign.Campaign
import com.pixelized.shared.lwa.model.campaign.CampaignJson
import com.pixelized.shared.lwa.model.campaign.CampaignJsonFactory
import com.pixelized.shared.lwa.protocol.payload.UpdatePlayerCharacteristicMessage
import com.pixelized.shared.lwa.model.campaign.character
import com.pixelized.shared.lwa.usecase.CampaignUseCase
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
class CampaignService(
@ -17,33 +19,102 @@ class CampaignService(
private val useCase: CampaignUseCase,
) {
private val scope = CoroutineScope(Dispatchers.IO + Job())
private val campaign = store.campaignFlow().stateIn(
scope = scope,
started = SharingStarted.Eagerly,
initialValue = Campaign.EMPTY,
)
private val campaign: Campaign get() = campaignFlow.value
private val campaignFlow = store.campaignFlow()
.stateIn(
scope = scope,
started = SharingStarted.Eagerly,
initialValue = Campaign.EMPTY,
)
private val campaignJsonFlow: StateFlow<CampaignJson> = campaignFlow
.map { factory.convertToJson(it) }
.stateIn(
scope = scope,
started = SharingStarted.Eagerly,
initialValue = factory.convertToJson(campaignFlow.value),
)
fun campaign(): CampaignJson {
return campaign.value.let(factory::convertToJson)
return campaignJsonFlow.value
}
suspend fun update(
message: UpdatePlayerCharacteristicMessage,
suspend fun addCharacter(characterId: String): Boolean {
// fetch all the current campaign character
val characters = campaign.characters.toMutableMap()
// check if the character is in the campaign.
if (characters.containsKey(characterId)) return false
// update the corresponding character
characters[characterId] = campaign.character(id = characterId)
// save the campaign to the disk + update the flow.
store.save(
campaign = campaign.copy(characters = characters)
)
return true
}
suspend fun removeCharacter(characterId: String): Boolean {
// fetch all the current campaign character
val characters = campaign.characters.toMutableMap()
// check if the character is in the campaign.
if (characters.containsKey(characterId).not()) return false
// update the corresponding character
characters.remove(characterId)
// save the campaign to the disk + update the flow.
store.save(
campaign = campaign.copy(characters = characters)
)
return true
}
suspend fun addNpc(npcId: String): Boolean {
// fetch all the current campaign character
val characters = campaign.npcs.toMutableMap()
// check if the character is in the campaign.
if (characters.containsKey(npcId)) return false
// update the corresponding character
characters[npcId] = campaign.character(id = npcId)
// save the campaign to the disk + update the flow.
store.save(
campaign = campaign.copy(npcs = characters)
)
return true
}
suspend fun removeNpc(npcId: String): Boolean {
// fetch all the current campaign character
val characters = campaign.npcs.toMutableMap()
// check if the character is in the campaign.
if (characters.containsKey(npcId).not()) return false
// update the corresponding character
characters.remove(npcId)
// save the campaign to the disk + update the flow.
store.save(
campaign = campaign.copy(npcs = characters)
)
return true
}
// Data manipulation threw WebSocket.
suspend fun updateCharacteristic(
characterId: String,
characteristic: Campaign.CharacterInstance.Characteristic,
value: Int,
) {
// fetch all the current campaign character
val characters = campaign.value.characters.toMutableMap()
// update the corresponding character using the usecase
characters[message.characterId] = useCase.updateCharacteristic(
character = characters[message.characterId] ?: Campaign.CharacterInstance(
characteristic = emptyMap(),
usedSkill = emptyList(),
),
characteristic = message.characteristic,
value = message.value,
val characters = campaign.characters.toMutableMap()
// update the corresponding character using the use case.
characters[characterId] = useCase.updateCharacteristic(
character = campaign.character(id = characterId),
characteristic = characteristic,
value = value,
)
// save the campaign to the disk + update the flow.
store.save(
campaign = campaign.value.copy(characters = characters)
campaign = campaign.copy(characters = characters)
)
}
}

View file

@ -1,12 +1,15 @@
package com.pixelized.server.lwa.model.character
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJson
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJsonFactory
import com.pixelized.shared.lwa.protocol.rest.CharacterPreviewJson
import com.pixelized.shared.lwa.usecase.CharacterSheetUseCase
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
class CharacterSheetService(
@ -15,22 +18,66 @@ class CharacterSheetService(
private val useCase: CharacterSheetUseCase,
) {
private val scope = CoroutineScope(Dispatchers.IO + Job())
private val sheets = store.characterSheetFlow().stateIn(
scope = scope,
started = SharingStarted.Eagerly,
initialValue = emptyList()
)
private val sheets get() = sheetsFlow.value
private val sheetsFlow = store.characterSheetFlow()
.map { entry -> entry.associateBy { character -> character.id } }
.stateIn(
scope = scope,
started = SharingStarted.Eagerly,
initialValue = emptyMap()
)
fun character(): List<CharacterSheetJson> {
return sheets.value.map(factory::convertToJson)
fun characters(): List<CharacterPreviewJson> {
return sheets.map { factory.convertToPreviewJson(sheet = it.value) }
}
fun characterSkillChange(
fun character(id: String): CharacterSheetJson? {
return sheets[id]?.let(factory::convertToJson)
}
suspend fun updateCharacter(character: CharacterSheetJson) {
return store.save(sheet = factory.convertFromJson(character))
}
fun deleteCharacter(characterId: String) : Boolean {
return store.delete(id = characterId)
}
// Data manipulation threw WebSocket.
fun updateCharacterLevel(
characterId: String,
level: Int,
) {
sheets[characterId]?.let { character ->
val update = useCase.updateLevel(
character = character,
level = level,
)
store.save(sheet = update)
}
}
fun updateCharacterSkillLevel(
characterId: String,
skillId: String,
level: Int,
) {
sheets[characterId]?.let { character ->
val update = useCase.updateSkillLevel(
character = character,
skillId = skillId,
level = level,
)
store.save(sheet = update)
}
}
fun updateCharacterSkillUsage(
characterId: String,
skillId: String,
) {
val character = sheets.value.firstOrNull { it.id == characterId }
if (character != null) {
sheets[characterId]?.let { character ->
val update = useCase.updateSkillUsage(
character = character,
skillId = skillId,

View file

@ -69,14 +69,17 @@ class CharacterSheetStore(
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()
val deleted = file.delete()
if (deleted) {
flow.value = flow.value.toMutableList()
.also { data ->
data.removeIf { it.id == id }
}
.sortedBy {
it.name
}
}
return deleted
}
@Throws(

View file

@ -2,37 +2,41 @@ package com.pixelized.server.lwa.server
import com.pixelized.server.lwa.model.campaign.CampaignService
import com.pixelized.server.lwa.model.character.CharacterSheetService
import com.pixelized.shared.lwa.protocol.Message
import com.pixelized.shared.lwa.protocol.MessageType
import com.pixelized.shared.lwa.protocol.payload.UpdatePlayerCharacteristicMessage
import com.pixelized.shared.lwa.protocol.payload.UpdateSkillUsageMessage
import kotlinx.serialization.json.Json
import com.pixelized.shared.lwa.protocol.websocket.Message
import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation
import com.pixelized.shared.lwa.protocol.websocket.payload.RollMessage
import com.pixelized.shared.lwa.protocol.websocket.payload.UpdatePlayerCharacteristicMessage
import com.pixelized.shared.lwa.protocol.websocket.payload.UpdateSkillUsageMessage
import kotlinx.coroutines.flow.MutableSharedFlow
class Engine(
private val characterService: CharacterSheetService,
private val campaignService: CampaignService,
private val json: Json,
val characterService: CharacterSheetService,
val campaignService: CampaignService,
) {
val webSocket = MutableSharedFlow<Message>()
suspend fun handle(message: Message) {
println(message)
when (val data = message.value) {
RestSynchronisation.Campaign -> Unit // TODO
when (message.type) {
MessageType.Roll -> {
Unit // Nothing to do here.
}
is RestSynchronisation.CharacterUpdate -> Unit // TODO
MessageType.UpdateSkillUsage -> {
val data: UpdateSkillUsageMessage = json.decodeFromString(message.value)
characterService.characterSkillChange(
characterId = data.characterId,
skillId = data.skillId
)
}
is RollMessage -> Unit // Nothing to do here.
MessageType.UpdatePlayerCharacteristic -> {
val data: UpdatePlayerCharacteristicMessage = json.decodeFromString(message.value)
campaignService.update(data)
}
is UpdatePlayerCharacteristicMessage -> campaignService.updateCharacteristic(
characterId = data.characterId,
characteristic = data.characteristic,
value = data.value,
)
is UpdateSkillUsageMessage -> characterService.updateCharacterSkillUsage(
characterId = data.characterId,
skillId = data.skillId
)
is RestSynchronisation.CharacterDelete -> characterService.deleteCharacter(
characterId = data.characterId,
)
}
}
}

View file

@ -3,12 +3,17 @@ package com.pixelized.server.lwa.server
import com.pixelized.server.lwa.extention.decodeFromFrame
import com.pixelized.server.lwa.extention.encodeToFrame
import com.pixelized.server.lwa.model.campaign.CampaignService
import com.pixelized.server.lwa.model.character.CharacterSheetService
import com.pixelized.server.lwa.server.rest.campaign.deleteCampaignCharacter
import com.pixelized.server.lwa.server.rest.campaign.deleteCampaignNpc
import com.pixelized.server.lwa.server.rest.campaign.getCampaign
import com.pixelized.server.lwa.server.rest.campaign.putCampaignCharacter
import com.pixelized.server.lwa.server.rest.campaign.putCampaignNpc
import com.pixelized.server.lwa.server.rest.character.deleteCharacter
import com.pixelized.server.lwa.server.rest.character.getCharacter
import com.pixelized.server.lwa.server.rest.character.getCharacters
import com.pixelized.server.lwa.server.rest.character.putCharacter
import com.pixelized.shared.lwa.SERVER_PORT
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
@ -16,9 +21,10 @@ 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.delete
import io.ktor.server.routing.get
import io.ktor.server.routing.put
import io.ktor.server.routing.route
import io.ktor.server.routing.routing
import io.ktor.server.websocket.WebSockets
import io.ktor.server.websocket.pingPeriod
@ -26,7 +32,6 @@ import io.ktor.server.websocket.timeout
import io.ktor.server.websocket.webSocket
import io.ktor.websocket.Frame
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json
@ -40,7 +45,6 @@ typealias Server = EmbeddedServer<NettyApplicationEngine, NettyApplicationEngine
class LocalServer {
private var server: Server? = null
private val outgoingMessageBuffer = MutableSharedFlow<Message>()
fun create(
port: Int = SERVER_PORT, // 16030
@ -54,6 +58,8 @@ class LocalServer {
}
val json by inject<Json>()
val engine by inject<Engine>()
install(ContentNegotiation) {
json(json)
}
@ -65,40 +71,57 @@ class LocalServer {
masking = false
}
val engine by inject<Engine>()
val characterService by inject<CharacterSheetService>()
val campaignService by inject<CampaignService>()
routing {
get(
path = "/",
body = {
call.respondText(contentType = ContentType.Text.Html) {
"""<html><body><ul>
<li><a href="http://127.0.0.1:16030/characters">characters</a></li>
<li><a href="http://127.0.0.1:16030/campaign">campaign</a></li>
</ul></body></html>"""
}
}
)
get(
path = "/characters",
body = {
call.respond(characterService.character())
},
body = engine.getCharacters(),
)
get(
path = "/campaign",
body = {
call.respond(campaignService.campaign())
route(path = "/character") {
get(
path = "/detail",
body = engine.getCharacter(),
)
put(
path = "/update",
body = engine.putCharacter(),
)
delete(
path = "/delete",
body = engine.deleteCharacter(),
)
}
route(path = "/campaign") {
get(
path = "",
body = engine.getCampaign(),
)
route(path = "/character") {
put(
path = "/update",
body = engine.putCampaignCharacter(),
)
delete(
path = "/delete",
body = engine.deleteCampaignCharacter(),
)
}
)
route(path = "/npc") {
put(
path = "/update",
body = engine.putCampaignNpc(),
)
delete(
path = "/delete",
body = engine.deleteCampaignNpc(),
)
}
}
webSocket(
path = "/ws",
handler = {
val job = launch {
// send local message to the clients
outgoingMessageBuffer.collect { message ->
engine.webSocket.collect { message ->
send(json.encodeToFrame(message))
}
}
@ -110,7 +133,7 @@ class LocalServer {
// log the message
engine.handle(message)
// broadcast to clients the message
outgoingMessageBuffer.emit(message)
engine.webSocket.emit(message)
}
}
}.onFailure { exception ->

View file

@ -0,0 +1,28 @@
package com.pixelized.server.lwa.server.rest.campaign
import com.pixelized.server.lwa.server.Engine
import com.pixelized.shared.lwa.protocol.websocket.Message
import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation
import io.ktor.http.HttpStatusCode
import io.ktor.server.response.respondText
fun Engine.deleteCampaignCharacter(): suspend io.ktor.server.routing.RoutingContext.() -> Unit {
return {
val id = call.queryParameters["id"]
val updated = id?.let { campaignService.removeCharacter(it) } ?: false
val code = when (updated) {
true -> HttpStatusCode.Accepted
else -> HttpStatusCode.UnprocessableEntity
}
call.respondText(
text = "$code",
status = code,
)
webSocket.emit(
Message(
from = "Server",
value = RestSynchronisation.Campaign,
)
)
}
}

View file

@ -0,0 +1,28 @@
package com.pixelized.server.lwa.server.rest.campaign
import com.pixelized.server.lwa.server.Engine
import com.pixelized.shared.lwa.protocol.websocket.Message
import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation
import io.ktor.http.HttpStatusCode
import io.ktor.server.response.respondText
fun Engine.deleteCampaignNpc(): suspend io.ktor.server.routing.RoutingContext.() -> Unit {
return {
val id = call.queryParameters["id"]
val updated = id?.let { campaignService.removeNpc(it) } ?: false
val code = when (updated) {
true -> HttpStatusCode.Accepted
else -> HttpStatusCode.UnprocessableEntity
}
call.respondText(
text = "$code",
status = code,
)
webSocket.emit(
Message(
from = "Server",
value = RestSynchronisation.Campaign,
)
)
}
}

View file

@ -0,0 +1,10 @@
package com.pixelized.server.lwa.server.rest.campaign
import com.pixelized.server.lwa.server.Engine
import io.ktor.server.response.respond
fun Engine.getCampaign(): suspend io.ktor.server.routing.RoutingContext.() -> Unit {
return {
call.respond(campaignService.campaign())
}
}

View file

@ -0,0 +1,28 @@
package com.pixelized.server.lwa.server.rest.campaign
import com.pixelized.server.lwa.server.Engine
import com.pixelized.shared.lwa.protocol.websocket.Message
import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation
import io.ktor.http.HttpStatusCode
import io.ktor.server.response.respondText
fun Engine.putCampaignCharacter(): suspend io.ktor.server.routing.RoutingContext.() -> Unit {
return {
val id = call.queryParameters["id"]
val updated = id?.let { campaignService.addCharacter(it) } ?: false
val code = when (updated) {
true -> HttpStatusCode.Accepted
else -> HttpStatusCode.UnprocessableEntity
}
call.respondText(
text = "$code",
status = code,
)
webSocket.emit(
Message(
from = "Server",
value = RestSynchronisation.Campaign,
)
)
}
}

View file

@ -0,0 +1,28 @@
package com.pixelized.server.lwa.server.rest.campaign
import com.pixelized.server.lwa.server.Engine
import com.pixelized.shared.lwa.protocol.websocket.Message
import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation
import io.ktor.http.HttpStatusCode
import io.ktor.server.response.respondText
fun Engine.putCampaignNpc(): suspend io.ktor.server.routing.RoutingContext.() -> Unit {
return {
val id = call.queryParameters["id"]
val updated = id?.let { campaignService.addNpc(it) } ?: false
val code = when (updated) {
true -> HttpStatusCode.Accepted
else -> HttpStatusCode.UnprocessableEntity
}
call.respondText(
text = "$code",
status = code,
)
webSocket.emit(
Message(
from = "Server",
value = RestSynchronisation.Campaign,
)
)
}
}

View file

@ -0,0 +1,32 @@
package com.pixelized.server.lwa.server.rest.character
import com.pixelized.server.lwa.server.Engine
import com.pixelized.shared.lwa.protocol.websocket.Message
import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation
import io.ktor.http.HttpStatusCode
import io.ktor.server.response.respondText
fun Engine.deleteCharacter(): suspend io.ktor.server.routing.RoutingContext.() -> Unit {
return {
val id = call.parameters["id"]
val deleted = id?.let(characterService::deleteCharacter) ?: false
if (deleted && id != null) {
call.respondText(
text = "${HttpStatusCode.OK}",
status = HttpStatusCode.OK,
)
webSocket.emit(
Message(
from = "Server",
value = RestSynchronisation.CharacterDelete(characterId = id),
)
)
} else {
call.respondText(
text = "${HttpStatusCode.UnprocessableEntity}",
status = HttpStatusCode.UnprocessableEntity,
)
}
}
}

View file

@ -0,0 +1,22 @@
package com.pixelized.server.lwa.server.rest.character
import com.pixelized.server.lwa.server.Engine
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJson
import io.ktor.http.HttpStatusCode
import io.ktor.server.response.respond
import io.ktor.server.response.respondText
fun Engine.getCharacter(): suspend io.ktor.server.routing.RoutingContext.() -> Unit {
return {
val id = call.queryParameters["id"]
val body: CharacterSheetJson? = id?.let(characterService::character)
if (body != null) {
call.respond(body)
} else {
call.respondText(
text = "${HttpStatusCode.UnprocessableEntity}",
status = HttpStatusCode.UnprocessableEntity
)
}
}
}

View file

@ -0,0 +1,10 @@
package com.pixelized.server.lwa.server.rest.character
import com.pixelized.server.lwa.server.Engine
import io.ktor.server.response.respond
fun Engine.getCharacters(): suspend io.ktor.server.routing.RoutingContext.() -> Unit {
return {
call.respond(characterService.characters())
}
}

View file

@ -0,0 +1,28 @@
package com.pixelized.server.lwa.server.rest.character
import com.pixelized.server.lwa.server.Engine
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJson
import com.pixelized.shared.lwa.protocol.websocket.Message
import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation
import io.ktor.http.HttpStatusCode
import io.ktor.server.request.receive
import io.ktor.server.response.respondText
fun Engine.putCharacter(): suspend io.ktor.server.routing.RoutingContext.() -> Unit {
return {
val form = call.receive<CharacterSheetJson>()
characterService.updateCharacter(
character = form
)
call.respondText(
text = "${HttpStatusCode.OK}",
status = HttpStatusCode.OK
)
webSocket.emit(
Message(
from = "Server",
value = RestSynchronisation.CharacterUpdate(id = form.id),
)
)
}
}

View file

@ -2,10 +2,10 @@ package com.pixelized.shared.lwa.model.campaign
data class Campaign(
val characters: Map<String, CharacterInstance>,
val npcs: Map<String, CharacterInstance>,
) {
data class CharacterInstance(
val characteristic: Map<Characteristic, Int>,
val usedSkill: List<String>,
) {
enum class Characteristic {
Damage,
@ -16,6 +16,7 @@ data class Campaign(
companion object {
val EMPTY = Campaign(
characters = emptyMap(),
npcs = emptyMap(),
)
}
}
@ -23,10 +24,12 @@ data class Campaign(
fun Campaign.character(id: String): Campaign.CharacterInstance {
return characters[id] ?: Campaign.CharacterInstance(
characteristic = emptyMap(),
usedSkill = emptyList(),
)
}
val Campaign.CharacterInstance.level
get() = characteristic[Campaign.CharacterInstance.Characteristic.Damage] ?: 1
val Campaign.CharacterInstance.damage
get() = characteristic[Campaign.CharacterInstance.Characteristic.Damage] ?: 0

View file

@ -14,17 +14,21 @@ class CampaignJsonFactory {
json: CampaignJsonV1,
): Campaign {
return Campaign(
characters = json.characters.map { entry ->
entry.key to Campaign.CharacterInstance(
characteristic = entry.value.characteristic.map { char ->
when (char.key) {
CampaignJsonV1.CharacterInstanceJson.Characteristic.Damage -> Campaign.CharacterInstance.Characteristic.Damage
CampaignJsonV1.CharacterInstanceJson.Characteristic.Power -> Campaign.CharacterInstance.Characteristic.Power
} to char.value
}.toMap(),
usedSkill = entry.value.usedSkill,
)
}.toMap()
characters = json.characters.mapValues { convertFromV1(json = it.value) },
npcs = json.npcs.mapValues { convertFromV1(json = it.value) },
)
}
private fun convertFromV1(
json: CampaignJsonV1.CharacterInstanceJson,
): Campaign.CharacterInstance {
return Campaign.CharacterInstance(
characteristic = json.characteristic.map { char ->
when (char.key) {
CampaignJsonV1.CharacterInstanceJson.Characteristic.Damage -> Campaign.CharacterInstance.Characteristic.Damage
CampaignJsonV1.CharacterInstanceJson.Characteristic.Power -> Campaign.CharacterInstance.Characteristic.Power
} to char.value
}.toMap(),
)
}
@ -32,17 +36,21 @@ class CampaignJsonFactory {
data: Campaign,
): CampaignJson {
return CampaignJsonV1(
characters = data.characters.map { entry ->
entry.key to CampaignJsonV1.CharacterInstanceJson(
characteristic = entry.value.characteristic.map { char ->
when (char.key) {
Campaign.CharacterInstance.Characteristic.Damage -> CampaignJsonV1.CharacterInstanceJson.Characteristic.Damage
Campaign.CharacterInstance.Characteristic.Power -> CampaignJsonV1.CharacterInstanceJson.Characteristic.Power
} to char.value
}.toMap(),
usedSkill = entry.value.usedSkill,
)
}.toMap()
characters = data.characters.mapValues { convertToJson(data = it.value) },
npcs = data.npcs.mapValues { convertToJson(data = it.value) },
)
}
private fun convertToJson(
data: Campaign.CharacterInstance,
): CampaignJsonV1.CharacterInstanceJson {
return CampaignJsonV1.CharacterInstanceJson(
characteristic = data.characteristic.map { char ->
when (char.key) {
Campaign.CharacterInstance.Characteristic.Damage -> CampaignJsonV1.CharacterInstanceJson.Characteristic.Damage
Campaign.CharacterInstance.Characteristic.Power -> CampaignJsonV1.CharacterInstanceJson.Characteristic.Power
} to char.value
}.toMap(),
)
}
}

View file

@ -5,12 +5,12 @@ import kotlinx.serialization.Serializable
@Serializable
data class CampaignJsonV1(
val characters: Map<String, CharacterInstanceJson>,
val npcs: Map<String, CharacterInstanceJson>,
) : CampaignJson {
@Serializable
data class CharacterInstanceJson(
val characteristic: Map<Characteristic, Int>,
val usedSkill: List<String>,
) {
enum class Characteristic {
Damage,

View file

@ -3,4 +3,6 @@ package com.pixelized.shared.lwa.model.characterSheet
import kotlinx.serialization.Serializable
@Serializable
sealed interface CharacterSheetJson
sealed interface CharacterSheetJson {
val id: String
}

View file

@ -1,5 +1,6 @@
package com.pixelized.shared.lwa.model.characterSheet
import com.pixelized.shared.lwa.protocol.rest.CharacterPreviewJson
import com.pixelized.shared.lwa.usecase.CharacterSheetUseCase
@ -98,6 +99,16 @@ class CharacterSheetJsonFactory(
)
}
fun convertToPreviewJson(
sheet: CharacterSheet,
): CharacterPreviewJson {
return CharacterPreviewJson(
id = sheet.id,
name = sheet.name,
level = sheet.level,
)
}
fun convertToJson(
sheet: CharacterSheet,
): CharacterSheetJson {

View file

@ -4,7 +4,7 @@ import kotlinx.serialization.Serializable
@Serializable
data class CharacterSheetJsonV1(
val id: String,
override val id: String,
val name: String,
val portrait: String?,
val thumbnail: String?,

View file

@ -1,10 +0,0 @@
package com.pixelized.shared.lwa.protocol
import kotlinx.serialization.Serializable
@Serializable
data class Message(
val from: String,
val type: MessageType,
val value: String,
)

View file

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

View file

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

View file

@ -0,0 +1,10 @@
package com.pixelized.shared.lwa.protocol.rest
import kotlinx.serialization.Serializable
@Serializable
class CharacterPreviewJson(
val id: String,
val name: String,
val level: Int,
)

View file

@ -0,0 +1,10 @@
package com.pixelized.shared.lwa.protocol.websocket
import com.pixelized.shared.lwa.protocol.websocket.payload.MessagePayload
import kotlinx.serialization.Serializable
@Serializable
data class Message(
val from: String,
val value: MessagePayload,
)

View file

@ -0,0 +1,6 @@
package com.pixelized.shared.lwa.protocol.websocket.payload
import kotlinx.serialization.Serializable
@Serializable
sealed interface MessagePayload

View file

@ -0,0 +1,20 @@
package com.pixelized.shared.lwa.protocol.websocket.payload
import kotlinx.serialization.Serializable
@Serializable
sealed class RestSynchronisation : MessagePayload {
@Serializable
data class CharacterUpdate(
val id: String,
) : RestSynchronisation()
@Serializable
data class CharacterDelete(
val characterId: String,
) : RestSynchronisation()
@Serializable
data object Campaign : RestSynchronisation()
}

View file

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

View file

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

View file

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

View file

@ -57,32 +57,49 @@ class CharacterSheetUseCase {
return (constitution / 3)
}
// Update character sheet.
fun updateLevel(
character: CharacterSheet,
level: Int,
): CharacterSheet {
return character.copy(
level = level,
)
}
fun updateSkillLevel(
character: CharacterSheet,
skillId: String,
level: Int,
): CharacterSheet {
return character.copy(
commonSkills = character.commonSkills.map { skill ->
skill.takeIf { skill.id == skillId }?.copy(level = level) ?: skill
},
specialSkills = character.specialSkills.map { skill ->
skill.takeIf { skill.id == skillId }?.copy(level = level) ?: skill
},
magicSkills = character.magicSkills.map { skill ->
skill.takeIf { skill.id == skillId }?.copy(level = level) ?: skill
},
)
}
fun updateSkillUsage(
character: CharacterSheet,
skillId: String,
): CharacterSheet {
return character.copy(
commonSkills = character.commonSkills.map { skill ->
if (skill.id == skillId) {
skill.copy(used = skill.used.not())
} else {
skill
}
skill.takeIf { skill.id == skillId }?.copy(used = skill.used.not()) ?: skill
},
specialSkills = character.specialSkills.map { skill ->
if (skill.id == skillId) {
skill.copy(used = skill.used.not())
} else {
skill
}
skill.takeIf { skill.id == skillId }?.copy(used = skill.used.not()) ?: skill
},
magicSkills = character.magicSkills.map { skill ->
if (skill.id == skillId) {
skill.copy(used = skill.used.not())
} else {
skill
}
}
skill.takeIf { skill.id == skillId }?.copy(used = skill.used.not()) ?: skill
},
)
}
}