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.CharacterSheetService
import com.pixelized.server.lwa.model.character.CharacterSheetStore import com.pixelized.server.lwa.model.character.CharacterSheetStore
import com.pixelized.server.lwa.server.Engine import com.pixelized.server.lwa.server.Engine
import org.koin.core.module.dsl.createdAtStart
import org.koin.core.module.dsl.singleOf import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module import org.koin.dsl.module
@ -18,7 +19,7 @@ val serverModuleDependencies
val engineDependencies val engineDependencies
get() = module { get() = module {
singleOf(::Engine) singleOf(constructor = ::Engine, options = { createdAtStart() })
} }
val storeDependencies val storeDependencies

View file

@ -1,6 +1,6 @@
package com.pixelized.server.lwa.extention 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.Frame
import io.ktor.websocket.readText import io.ktor.websocket.readText
import kotlinx.serialization.json.Json 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.Campaign
import com.pixelized.shared.lwa.model.campaign.CampaignJson import com.pixelized.shared.lwa.model.campaign.CampaignJson
import com.pixelized.shared.lwa.model.campaign.CampaignJsonFactory 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 com.pixelized.shared.lwa.usecase.CampaignUseCase
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
class CampaignService( class CampaignService(
@ -17,33 +19,102 @@ class CampaignService(
private val useCase: CampaignUseCase, private val useCase: CampaignUseCase,
) { ) {
private val scope = CoroutineScope(Dispatchers.IO + Job()) private val scope = CoroutineScope(Dispatchers.IO + Job())
private val campaign = store.campaignFlow().stateIn(
scope = scope, private val campaign: Campaign get() = campaignFlow.value
started = SharingStarted.Eagerly,
initialValue = Campaign.EMPTY, 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 { fun campaign(): CampaignJson {
return campaign.value.let(factory::convertToJson) return campaignJsonFlow.value
} }
suspend fun update( suspend fun addCharacter(characterId: String): Boolean {
message: UpdatePlayerCharacteristicMessage, // 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 // fetch all the current campaign character
val characters = campaign.value.characters.toMutableMap() val characters = campaign.characters.toMutableMap()
// update the corresponding character using the usecase // update the corresponding character using the use case.
characters[message.characterId] = useCase.updateCharacteristic( characters[characterId] = useCase.updateCharacteristic(
character = characters[message.characterId] ?: Campaign.CharacterInstance( character = campaign.character(id = characterId),
characteristic = emptyMap(), characteristic = characteristic,
usedSkill = emptyList(), value = value,
),
characteristic = message.characteristic,
value = message.value,
) )
// save the campaign to the disk + update the flow. // save the campaign to the disk + update the flow.
store.save( 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 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.CharacterSheetJson
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJsonFactory import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJsonFactory
import com.pixelized.shared.lwa.protocol.rest.CharacterPreviewJson
import com.pixelized.shared.lwa.usecase.CharacterSheetUseCase import com.pixelized.shared.lwa.usecase.CharacterSheetUseCase
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
class CharacterSheetService( class CharacterSheetService(
@ -15,22 +18,66 @@ class CharacterSheetService(
private val useCase: CharacterSheetUseCase, private val useCase: CharacterSheetUseCase,
) { ) {
private val scope = CoroutineScope(Dispatchers.IO + Job()) private val scope = CoroutineScope(Dispatchers.IO + Job())
private val sheets = store.characterSheetFlow().stateIn( private val sheets get() = sheetsFlow.value
scope = scope, private val sheetsFlow = store.characterSheetFlow()
started = SharingStarted.Eagerly, .map { entry -> entry.associateBy { character -> character.id } }
initialValue = emptyList() .stateIn(
) scope = scope,
started = SharingStarted.Eagerly,
initialValue = emptyMap()
)
fun character(): List<CharacterSheetJson> { fun characters(): List<CharacterPreviewJson> {
return sheets.value.map(factory::convertToJson) 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, characterId: String,
skillId: String, skillId: String,
) { ) {
val character = sheets.value.firstOrNull { it.id == characterId } sheets[characterId]?.let { character ->
if (character != null) {
val update = useCase.updateSkillUsage( val update = useCase.updateSkillUsage(
character = character, character = character,
skillId = skillId, skillId = skillId,

View file

@ -69,14 +69,17 @@ class CharacterSheetStore(
fun delete(id: String): Boolean { fun delete(id: String): Boolean {
val file = characterSheetFile(id = id) val file = characterSheetFile(id = id)
flow.value = flow.value.toMutableList() val deleted = file.delete()
.also { data -> if (deleted) {
data.removeIf { it.id == id } flow.value = flow.value.toMutableList()
} .also { data ->
.sortedBy { data.removeIf { it.id == id }
it.name }
} .sortedBy {
return file.delete() it.name
}
}
return deleted
} }
@Throws( @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.campaign.CampaignService
import com.pixelized.server.lwa.model.character.CharacterSheetService import com.pixelized.server.lwa.model.character.CharacterSheetService
import com.pixelized.shared.lwa.protocol.Message import com.pixelized.shared.lwa.protocol.websocket.Message
import com.pixelized.shared.lwa.protocol.MessageType import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation
import com.pixelized.shared.lwa.protocol.payload.UpdatePlayerCharacteristicMessage import com.pixelized.shared.lwa.protocol.websocket.payload.RollMessage
import com.pixelized.shared.lwa.protocol.payload.UpdateSkillUsageMessage import com.pixelized.shared.lwa.protocol.websocket.payload.UpdatePlayerCharacteristicMessage
import kotlinx.serialization.json.Json import com.pixelized.shared.lwa.protocol.websocket.payload.UpdateSkillUsageMessage
import kotlinx.coroutines.flow.MutableSharedFlow
class Engine( class Engine(
private val characterService: CharacterSheetService, val characterService: CharacterSheetService,
private val campaignService: CampaignService, val campaignService: CampaignService,
private val json: Json,
) { ) {
val webSocket = MutableSharedFlow<Message>()
suspend fun handle(message: Message) { suspend fun handle(message: Message) {
println(message) when (val data = message.value) {
RestSynchronisation.Campaign -> Unit // TODO
when (message.type) { is RestSynchronisation.CharacterUpdate -> Unit // TODO
MessageType.Roll -> {
Unit // Nothing to do here.
}
MessageType.UpdateSkillUsage -> { is RollMessage -> Unit // Nothing to do here.
val data: UpdateSkillUsageMessage = json.decodeFromString(message.value)
characterService.characterSkillChange(
characterId = data.characterId,
skillId = data.skillId
)
}
MessageType.UpdatePlayerCharacteristic -> { is UpdatePlayerCharacteristicMessage -> campaignService.updateCharacteristic(
val data: UpdatePlayerCharacteristicMessage = json.decodeFromString(message.value) characterId = data.characterId,
campaignService.update(data) 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.decodeFromFrame
import com.pixelized.server.lwa.extention.encodeToFrame import com.pixelized.server.lwa.extention.encodeToFrame
import com.pixelized.server.lwa.model.campaign.CampaignService import com.pixelized.server.lwa.server.rest.campaign.deleteCampaignCharacter
import com.pixelized.server.lwa.model.character.CharacterSheetService 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.SERVER_PORT
import com.pixelized.shared.lwa.protocol.Message
import com.pixelized.shared.lwa.sharedModuleDependencies import com.pixelized.shared.lwa.sharedModuleDependencies
import io.ktor.http.ContentType
import io.ktor.serialization.kotlinx.json.json import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.install import io.ktor.server.application.install
import io.ktor.server.engine.EmbeddedServer import io.ktor.server.engine.EmbeddedServer
@ -16,9 +21,10 @@ import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty import io.ktor.server.netty.Netty
import io.ktor.server.netty.NettyApplicationEngine import io.ktor.server.netty.NettyApplicationEngine
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
import io.ktor.server.response.respond import io.ktor.server.routing.delete
import io.ktor.server.response.respondText
import io.ktor.server.routing.get 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.routing.routing
import io.ktor.server.websocket.WebSockets import io.ktor.server.websocket.WebSockets
import io.ktor.server.websocket.pingPeriod 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.server.websocket.webSocket
import io.ktor.websocket.Frame import io.ktor.websocket.Frame
import kotlinx.coroutines.channels.consumeEach import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@ -40,7 +45,6 @@ typealias Server = EmbeddedServer<NettyApplicationEngine, NettyApplicationEngine
class LocalServer { class LocalServer {
private var server: Server? = null private var server: Server? = null
private val outgoingMessageBuffer = MutableSharedFlow<Message>()
fun create( fun create(
port: Int = SERVER_PORT, // 16030 port: Int = SERVER_PORT, // 16030
@ -54,6 +58,8 @@ class LocalServer {
} }
val json by inject<Json>() val json by inject<Json>()
val engine by inject<Engine>()
install(ContentNegotiation) { install(ContentNegotiation) {
json(json) json(json)
} }
@ -65,40 +71,57 @@ class LocalServer {
masking = false masking = false
} }
val engine by inject<Engine>()
val characterService by inject<CharacterSheetService>()
val campaignService by inject<CampaignService>()
routing { 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( get(
path = "/characters", path = "/characters",
body = { body = engine.getCharacters(),
call.respond(characterService.character())
},
) )
get( route(path = "/character") {
path = "/campaign", get(
body = { path = "/detail",
call.respond(campaignService.campaign()) 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( webSocket(
path = "/ws", path = "/ws",
handler = { handler = {
val job = launch { val job = launch {
// send local message to the clients // send local message to the clients
outgoingMessageBuffer.collect { message -> engine.webSocket.collect { message ->
send(json.encodeToFrame(message)) send(json.encodeToFrame(message))
} }
} }
@ -110,7 +133,7 @@ class LocalServer {
// log the message // log the message
engine.handle(message) engine.handle(message)
// broadcast to clients the message // broadcast to clients the message
outgoingMessageBuffer.emit(message) engine.webSocket.emit(message)
} }
} }
}.onFailure { exception -> }.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( data class Campaign(
val characters: Map<String, CharacterInstance>, val characters: Map<String, CharacterInstance>,
val npcs: Map<String, CharacterInstance>,
) { ) {
data class CharacterInstance( data class CharacterInstance(
val characteristic: Map<Characteristic, Int>, val characteristic: Map<Characteristic, Int>,
val usedSkill: List<String>,
) { ) {
enum class Characteristic { enum class Characteristic {
Damage, Damage,
@ -16,6 +16,7 @@ data class Campaign(
companion object { companion object {
val EMPTY = Campaign( val EMPTY = Campaign(
characters = emptyMap(), characters = emptyMap(),
npcs = emptyMap(),
) )
} }
} }
@ -23,10 +24,12 @@ data class Campaign(
fun Campaign.character(id: String): Campaign.CharacterInstance { fun Campaign.character(id: String): Campaign.CharacterInstance {
return characters[id] ?: Campaign.CharacterInstance( return characters[id] ?: Campaign.CharacterInstance(
characteristic = emptyMap(), characteristic = emptyMap(),
usedSkill = emptyList(),
) )
} }
val Campaign.CharacterInstance.level
get() = characteristic[Campaign.CharacterInstance.Characteristic.Damage] ?: 1
val Campaign.CharacterInstance.damage val Campaign.CharacterInstance.damage
get() = characteristic[Campaign.CharacterInstance.Characteristic.Damage] ?: 0 get() = characteristic[Campaign.CharacterInstance.Characteristic.Damage] ?: 0

View file

@ -14,17 +14,21 @@ class CampaignJsonFactory {
json: CampaignJsonV1, json: CampaignJsonV1,
): Campaign { ): Campaign {
return Campaign( return Campaign(
characters = json.characters.map { entry -> characters = json.characters.mapValues { convertFromV1(json = it.value) },
entry.key to Campaign.CharacterInstance( npcs = json.npcs.mapValues { convertFromV1(json = it.value) },
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 private fun convertFromV1(
} to char.value json: CampaignJsonV1.CharacterInstanceJson,
}.toMap(), ): Campaign.CharacterInstance {
usedSkill = entry.value.usedSkill, return Campaign.CharacterInstance(
) characteristic = json.characteristic.map { char ->
}.toMap() 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, data: Campaign,
): CampaignJson { ): CampaignJson {
return CampaignJsonV1( return CampaignJsonV1(
characters = data.characters.map { entry -> characters = data.characters.mapValues { convertToJson(data = it.value) },
entry.key to CampaignJsonV1.CharacterInstanceJson( npcs = data.npcs.mapValues { convertToJson(data = it.value) },
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 private fun convertToJson(
} to char.value data: Campaign.CharacterInstance,
}.toMap(), ): CampaignJsonV1.CharacterInstanceJson {
usedSkill = entry.value.usedSkill, return CampaignJsonV1.CharacterInstanceJson(
) characteristic = data.characteristic.map { char ->
}.toMap() 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 @Serializable
data class CampaignJsonV1( data class CampaignJsonV1(
val characters: Map<String, CharacterInstanceJson>, val characters: Map<String, CharacterInstanceJson>,
val npcs: Map<String, CharacterInstanceJson>,
) : CampaignJson { ) : CampaignJson {
@Serializable @Serializable
data class CharacterInstanceJson( data class CharacterInstanceJson(
val characteristic: Map<Characteristic, Int>, val characteristic: Map<Characteristic, Int>,
val usedSkill: List<String>,
) { ) {
enum class Characteristic { enum class Characteristic {
Damage, Damage,

View file

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

View file

@ -1,5 +1,6 @@
package com.pixelized.shared.lwa.model.characterSheet package com.pixelized.shared.lwa.model.characterSheet
import com.pixelized.shared.lwa.protocol.rest.CharacterPreviewJson
import com.pixelized.shared.lwa.usecase.CharacterSheetUseCase 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( fun convertToJson(
sheet: CharacterSheet, sheet: CharacterSheet,
): CharacterSheetJson { ): CharacterSheetJson {

View file

@ -4,7 +4,7 @@ import kotlinx.serialization.Serializable
@Serializable @Serializable
data class CharacterSheetJsonV1( data class CharacterSheetJsonV1(
val id: String, override val id: String,
val name: String, val name: String,
val portrait: String?, val portrait: String?,
val thumbnail: 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 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 com.pixelized.shared.lwa.model.campaign.Campaign
import kotlinx.serialization.Serializable 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 import kotlinx.serialization.Serializable

View file

@ -57,32 +57,49 @@ class CharacterSheetUseCase {
return (constitution / 3) 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( fun updateSkillUsage(
character: CharacterSheet, character: CharacterSheet,
skillId: String, skillId: String,
): CharacterSheet { ): CharacterSheet {
return character.copy( return character.copy(
commonSkills = character.commonSkills.map { skill -> commonSkills = character.commonSkills.map { skill ->
if (skill.id == skillId) { skill.takeIf { skill.id == skillId }?.copy(used = skill.used.not()) ?: skill
skill.copy(used = skill.used.not())
} else {
skill
}
}, },
specialSkills = character.specialSkills.map { skill -> specialSkills = character.specialSkills.map { skill ->
if (skill.id == skillId) { skill.takeIf { skill.id == skillId }?.copy(used = skill.used.not()) ?: skill
skill.copy(used = skill.used.not())
} else {
skill
}
}, },
magicSkills = character.magicSkills.map { skill -> magicSkills = character.magicSkills.map { skill ->
if (skill.id == skillId) { skill.takeIf { skill.id == skillId }?.copy(used = skill.used.not()) ?: skill
skill.copy(used = skill.used.not()) },
} else {
skill
}
}
) )
} }
} }