diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/CampaignScreen.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/CampaignScreen.kt similarity index 100% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/CampaignScreen.kt rename to composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/CampaignScreen.kt diff --git a/server/src/main/kotlin/Module.kt b/server/src/main/kotlin/Module.kt index 083f000..4fba9bc 100644 --- a/server/src/main/kotlin/Module.kt +++ b/server/src/main/kotlin/Module.kt @@ -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 diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/extention/JsonExt.kt b/server/src/main/kotlin/com/pixelized/server/lwa/extention/JsonExt.kt index b46610e..8c2f2c2 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/extention/JsonExt.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/extention/JsonExt.kt @@ -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 diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/campaign/CampaignService.kt b/server/src/main/kotlin/com/pixelized/server/lwa/model/campaign/CampaignService.kt index d1665b0..b346260 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/campaign/CampaignService.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/model/campaign/CampaignService.kt @@ -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 = 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) ) } } \ No newline at end of file diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/character/CharacterSheetService.kt b/server/src/main/kotlin/com/pixelized/server/lwa/model/character/CharacterSheetService.kt index 92b0649..8bd0312 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/character/CharacterSheetService.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/model/character/CharacterSheetService.kt @@ -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 { - return sheets.value.map(factory::convertToJson) + fun characters(): List { + 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, diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/character/CharacterSheetStore.kt b/server/src/main/kotlin/com/pixelized/server/lwa/model/character/CharacterSheetStore.kt index 6a61e6f..97c8b44 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/character/CharacterSheetStore.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/model/character/CharacterSheetStore.kt @@ -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( diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/Engine.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/Engine.kt index a94e6f4..68be7d8 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/server/Engine.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/Engine.kt @@ -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() + 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, + ) } } } \ No newline at end of file diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/Server.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/Server.kt index 56c4c18..f6a8cb9 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/server/Server.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/Server.kt @@ -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() fun create( port: Int = SERVER_PORT, // 16030 @@ -54,6 +58,8 @@ class LocalServer { } val json by inject() + val engine by inject() + install(ContentNegotiation) { json(json) } @@ -65,40 +71,57 @@ class LocalServer { masking = false } - val engine by inject() - val characterService by inject() - val campaignService by inject() - routing { - get( - path = "/", - body = { - call.respondText(contentType = ContentType.Text.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 -> diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/DELETE_Campaign_character.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/DELETE_Campaign_character.kt new file mode 100644 index 0000000..60ac43a --- /dev/null +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/DELETE_Campaign_character.kt @@ -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, + ) + ) + } +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/DELETE_Campaign_npc.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/DELETE_Campaign_npc.kt new file mode 100644 index 0000000..b41781f --- /dev/null +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/DELETE_Campaign_npc.kt @@ -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, + ) + ) + } +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/GET_Campaign.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/GET_Campaign.kt new file mode 100644 index 0000000..e2a949c --- /dev/null +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/GET_Campaign.kt @@ -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()) + } +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/PUT_Campaign_character.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/PUT_Campaign_character.kt new file mode 100644 index 0000000..3d5d929 --- /dev/null +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/PUT_Campaign_character.kt @@ -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, + ) + ) + } +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/PUT_Campaign_npc.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/PUT_Campaign_npc.kt new file mode 100644 index 0000000..1995107 --- /dev/null +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/PUT_Campaign_npc.kt @@ -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, + ) + ) + } +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/character/DELETE_Character.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/character/DELETE_Character.kt new file mode 100644 index 0000000..d60bd49 --- /dev/null +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/character/DELETE_Character.kt @@ -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, + ) + } + } +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/character/GET_Character.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/character/GET_Character.kt new file mode 100644 index 0000000..8ab2351 --- /dev/null +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/character/GET_Character.kt @@ -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 + ) + } + } +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/character/GET_Characters.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/character/GET_Characters.kt new file mode 100644 index 0000000..0198dc0 --- /dev/null +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/character/GET_Characters.kt @@ -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()) + } +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/character/PUT_Character.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/character/PUT_Character.kt new file mode 100644 index 0000000..c30f801 --- /dev/null +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/character/PUT_Character.kt @@ -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() + characterService.updateCharacter( + character = form + ) + call.respondText( + text = "${HttpStatusCode.OK}", + status = HttpStatusCode.OK + ) + webSocket.emit( + Message( + from = "Server", + value = RestSynchronisation.CharacterUpdate(id = form.id), + ) + ) + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/Campaign.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/Campaign.kt index 83cd017..54524fb 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/Campaign.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/Campaign.kt @@ -2,10 +2,10 @@ package com.pixelized.shared.lwa.model.campaign data class Campaign( val characters: Map, + val npcs: Map, ) { data class CharacterInstance( val characteristic: Map, - val usedSkill: List, ) { 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 diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/CampaignJsonFactory.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/CampaignJsonFactory.kt index e008c34..9bd1acc 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/CampaignJsonFactory.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/CampaignJsonFactory.kt @@ -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(), ) } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/CampaignJsonV1.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/CampaignJsonV1.kt index 9e24bc3..d9fa986 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/CampaignJsonV1.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/CampaignJsonV1.kt @@ -5,12 +5,12 @@ import kotlinx.serialization.Serializable @Serializable data class CampaignJsonV1( val characters: Map, + val npcs: Map, ) : CampaignJson { @Serializable data class CharacterInstanceJson( val characteristic: Map, - val usedSkill: List, ) { enum class Characteristic { Damage, diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/characterSheet/CharacterSheetJson.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/characterSheet/CharacterSheetJson.kt index ee903ad..0055d07 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/characterSheet/CharacterSheetJson.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/characterSheet/CharacterSheetJson.kt @@ -3,4 +3,6 @@ package com.pixelized.shared.lwa.model.characterSheet import kotlinx.serialization.Serializable @Serializable -sealed interface CharacterSheetJson \ No newline at end of file +sealed interface CharacterSheetJson { + val id: String +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/characterSheet/CharacterSheetJsonFactory.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/characterSheet/CharacterSheetJsonFactory.kt index 192f6e7..ecc07fa 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/characterSheet/CharacterSheetJsonFactory.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/characterSheet/CharacterSheetJsonFactory.kt @@ -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 { diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/characterSheet/CharacterSheetJsonV1.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/characterSheet/CharacterSheetJsonV1.kt index 0339be9..11859f4 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/characterSheet/CharacterSheetJsonV1.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/characterSheet/CharacterSheetJsonV1.kt @@ -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?, diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/Message.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/Message.kt deleted file mode 100644 index e1ddaa5..0000000 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/Message.kt +++ /dev/null @@ -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, -) \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/MessageType.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/MessageType.kt deleted file mode 100644 index 96d19d5..0000000 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/MessageType.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.pixelized.shared.lwa.protocol - -enum class MessageType { - Roll, - UpdateSkillUsage, - UpdatePlayerCharacteristic, -} diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/payload/MessagePayload.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/payload/MessagePayload.kt deleted file mode 100644 index b0172f3..0000000 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/payload/MessagePayload.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.pixelized.shared.lwa.protocol.payload - -sealed interface MessagePayload \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/rest/CharacterPreviewJson.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/rest/CharacterPreviewJson.kt new file mode 100644 index 0000000..632d77b --- /dev/null +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/rest/CharacterPreviewJson.kt @@ -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, +) \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/Message.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/Message.kt new file mode 100644 index 0000000..518d9aa --- /dev/null +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/Message.kt @@ -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, +) \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/payload/MessagePayload.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/payload/MessagePayload.kt new file mode 100644 index 0000000..b9eaa8a --- /dev/null +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/payload/MessagePayload.kt @@ -0,0 +1,6 @@ +package com.pixelized.shared.lwa.protocol.websocket.payload + +import kotlinx.serialization.Serializable + +@Serializable +sealed interface MessagePayload \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/payload/RestSynchronisation.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/payload/RestSynchronisation.kt new file mode 100644 index 0000000..de1bfee --- /dev/null +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/payload/RestSynchronisation.kt @@ -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() +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/payload/RollMessage.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/payload/RollMessage.kt similarity index 82% rename from shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/payload/RollMessage.kt rename to shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/payload/RollMessage.kt index 7da99e8..d88a71e 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/payload/RollMessage.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/payload/RollMessage.kt @@ -1,4 +1,4 @@ -package com.pixelized.shared.lwa.protocol.payload +package com.pixelized.shared.lwa.protocol.websocket.payload import kotlinx.serialization.Serializable diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/payload/UpdatePlayerCharacteristicMessage.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/payload/UpdatePlayerCharacteristicMessage.kt similarity index 83% rename from shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/payload/UpdatePlayerCharacteristicMessage.kt rename to shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/payload/UpdatePlayerCharacteristicMessage.kt index 676de17..ef04b78 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/payload/UpdatePlayerCharacteristicMessage.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/payload/UpdatePlayerCharacteristicMessage.kt @@ -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 diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/payload/UpdateSkillUsageMessage.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/payload/UpdateSkillUsageMessage.kt similarity index 73% rename from shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/payload/UpdateSkillUsageMessage.kt rename to shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/payload/UpdateSkillUsageMessage.kt index 76592b3..a3a0e17 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/payload/UpdateSkillUsageMessage.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/payload/UpdateSkillUsageMessage.kt @@ -1,4 +1,4 @@ -package com.pixelized.shared.lwa.protocol.payload +package com.pixelized.shared.lwa.protocol.websocket.payload import kotlinx.serialization.Serializable diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/usecase/CharacterSheetUseCase.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/usecase/CharacterSheetUseCase.kt index aa855ce..55dead3 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/usecase/CharacterSheetUseCase.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/usecase/CharacterSheetUseCase.kt @@ -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 + }, ) } }