Add GM & NPC (UI) support. Change the Id system.

This commit is contained in:
Thomas Andres Gomez 2025-03-15 17:49:12 +01:00
parent 6b86a6c075
commit 27dba5438e
54 changed files with 816 additions and 426 deletions

View file

@ -30,15 +30,15 @@ class CharacterSheetService(
return sheets.map { factory.convertToPreviewJson(sheet = it.value) }
}
fun character(id: String): CharacterSheetJson? {
fun characterSheet(id: String): CharacterSheetJson? {
return sheets[id]?.let(factory::convertToJson)
}
suspend fun updateCharacter(character: CharacterSheetJson) {
suspend fun updateCharacterSheet(character: CharacterSheetJson) {
return store.save(sheet = factory.convertFromJson(character))
}
fun deleteCharacter(characterId: String): Boolean {
fun deleteCharacterSheet(characterId: String): Boolean {
return store.delete(id = characterId)
}

View file

@ -27,6 +27,7 @@ class Engine(
is CampaignMessage -> {
val instanceId = Campaign.CharacterInstance.Id(
prefix = data.prefix,
characterSheetId = data.characterSheetId,
instanceId = data.instanceId,
)

View file

@ -4,7 +4,7 @@ package com.pixelized.server.lwa.server
import com.pixelized.server.lwa.server.rest.alteration.getActiveAlteration
import com.pixelized.server.lwa.server.rest.alteration.getAlteration
import com.pixelized.server.lwa.server.rest.alteration.putActiveAlteration
import com.pixelized.server.lwa.server.rest.campaign.deleteCampaignCharacter
import com.pixelized.server.lwa.server.rest.campaign.removeCampaignCharacter
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
@ -142,7 +142,7 @@ class LocalServer {
)
delete(
path = "/delete",
body = engine.deleteCampaignCharacter(),
body = engine.removeCampaignCharacter(),
)
}
route(path = "/npc") {

View file

@ -1,26 +1,25 @@
package com.pixelized.server.lwa.server.rest.alteration
import com.pixelized.server.lwa.server.Engine
import com.pixelized.shared.lwa.model.campaign.Campaign
import com.pixelized.server.lwa.utils.extentions.characterInstanceId
import io.ktor.http.HttpStatusCode
import io.ktor.server.response.respond
import io.ktor.server.response.respondText
fun Engine.getActiveAlteration(): suspend io.ktor.server.routing.RoutingContext.() -> Unit {
return {
// get the query parameter
val characterSheetId = call.queryParameters["characterSheetId"]
val instanceId = call.queryParameters["instanceId"]?.toIntOrNull()
// build the character instance id.
val id = if (characterSheetId != null && instanceId != null) {
Campaign.CharacterInstance.Id(
characterSheetId = characterSheetId,
instanceId = instanceId
try {
// get the query parameter
val characterInstanceId = call.queryParameters.characterInstanceId
// fetch the data from the service
val data = alterationService.active(characterInstanceId = characterInstanceId)
// respond to the client.
call.respond(data)
} catch (exception: Exception) {
call.respondText(
text = exception.localizedMessage,
status = HttpStatusCode.UnprocessableEntity,
)
} else {
null
}
// fetch the data from the service
val data = id?.let { alterationService.active(it) } ?: emptyList()
// respond to the client.
call.respond(data)
}
}

View file

@ -1,7 +1,7 @@
package com.pixelized.server.lwa.server.rest.alteration
import com.pixelized.server.lwa.server.Engine
import com.pixelized.shared.lwa.model.campaign.Campaign
import com.pixelized.server.lwa.utils.extentions.characterInstanceId
import com.pixelized.shared.lwa.protocol.websocket.Message
import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation
import io.ktor.http.HttpStatusCode
@ -10,54 +10,45 @@ import io.ktor.server.response.respondText
fun Engine.putActiveAlteration(): suspend io.ktor.server.routing.RoutingContext.() -> Unit {
return {
// fetch the query parameters
val characterSheetId = call.queryParameters["characterSheetId"]
val instanceId = call.queryParameters["instanceId"]?.toIntOrNull()
val alterationId = call.receive<String>()
try {
// get the query parameter
val characterInstanceId = call.queryParameters.characterInstanceId
// build the characterInstanceId from the parameters
val characterInstanceId = if (characterSheetId != null && instanceId != null) {
Campaign.CharacterInstance.Id(
characterSheetId = characterSheetId,
instanceId = instanceId
)
} else {
null
}
// fetch the query parameters
val alterationId = call.receive<String>()
// Update the alteration
val updated = characterInstanceId?.let {
alterationService.toggleActiveAlteration(
characterInstanceId = it,
// Update the alteration
val updated = alterationService.toggleActiveAlteration(
characterInstanceId = characterInstanceId,
alterationId = alterationId,
)
} ?: false
// build the Http response & send it
val code = when (updated) {
true -> HttpStatusCode.Accepted
else -> HttpStatusCode.UnprocessableEntity
}
call.respondText(
text = "$code",
status = code,
)
// share the modification to all client through the websocket.
characterInstanceId?.let {
if (!updated) {
error("Unexpected error occurred when toggling the alteration (id:$alterationId) for the character (id:$characterInstanceId)")
}
// build the Http response & send it
call.respondText(
text = "$HttpStatusCode.Accepted",
status = HttpStatusCode.Accepted,
)
// share the modification to all client through the websocket.
webSocket.emit(
Message(
from = "Server",
value = RestSynchronisation.ToggleActiveAlteration(
characterId = campaignJsonFactory.convertToJson(id = it),
characterId = campaignJsonFactory.convertToJson(id = characterInstanceId),
alterationId = alterationId,
active = alterationService.isAlterationActive(
characterInstanceId = it,
characterInstanceId = characterInstanceId,
alterationId = alterationId
),
),
)
)
} catch (exception: Exception) {
call.respondText(
text = exception.localizedMessage,
status = HttpStatusCode.UnprocessableEntity,
)
}
}
}

View file

@ -1,38 +1,39 @@
package com.pixelized.server.lwa.server.rest.campaign
import com.pixelized.server.lwa.server.Engine
import com.pixelized.shared.lwa.model.campaign.Campaign
import com.pixelized.server.lwa.utils.extentions.characterInstanceId
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 {
fun Engine.removeCampaignCharacter(): suspend io.ktor.server.routing.RoutingContext.() -> Unit {
return {
val characterSheetId = call.queryParameters["characterSheetId"]
val instanceId = call.queryParameters["instanceId"]?.toIntOrNull()
val id = if (characterSheetId != null && instanceId != null) {
Campaign.CharacterInstance.Id(
characterSheetId = characterSheetId,
instanceId = instanceId
try {
// get the query parameter
val characterInstanceId = call.queryParameters.characterInstanceId
// remove the character form the party
val updated = campaignService.removeCharacter(characterInstanceId = characterInstanceId)
// error case
if (!updated) {
error("Unexpected error when removing character (id:$characterInstanceId) from party.")
}
// API & WebSocket responses
call.respondText(
text = "$HttpStatusCode.Accepted",
status = HttpStatusCode.Accepted,
)
} else {
null
}
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,
webSocket.emit(
Message(
from = "Server",
value = RestSynchronisation.Campaign,
)
)
)
} catch (exception: Exception) {
call.respondText(
text = exception.localizedMessage,
status = HttpStatusCode.UnprocessableEntity,
)
}
}
}

View file

@ -1,7 +1,7 @@
package com.pixelized.server.lwa.server.rest.campaign
import com.pixelized.server.lwa.server.Engine
import com.pixelized.shared.lwa.model.campaign.Campaign
import com.pixelized.server.lwa.utils.extentions.characterInstanceId
import com.pixelized.shared.lwa.protocol.websocket.Message
import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation
import io.ktor.http.HttpStatusCode
@ -9,30 +9,31 @@ import io.ktor.server.response.respondText
fun Engine.deleteCampaignNpc(): suspend io.ktor.server.routing.RoutingContext.() -> Unit {
return {
val characterSheetId = call.queryParameters["characterSheetId"]
val instanceId = call.queryParameters["instanceId"]?.toIntOrNull()
val id = if (characterSheetId != null && instanceId != null) {
Campaign.CharacterInstance.Id(
characterSheetId = characterSheetId,
instanceId = instanceId
try {
// get the query parameter
val characterInstanceId = call.queryParameters.characterInstanceId
// remove the character form the party
val updated = campaignService.removeNpc(npcInstanceId = characterInstanceId)
// error case
if (!updated) {
error("Unexpected error when removing character (id:$characterInstanceId) from npcs.")
}
// API & WebSocket responses
call.respondText(
text = "$HttpStatusCode.Accepted",
status = HttpStatusCode.Accepted,
)
} else {
null
}
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,
webSocket.emit(
Message(
from = "Server",
value = RestSynchronisation.Campaign,
)
)
)
} catch (exception: Exception) {
call.respondText(
text = exception.localizedMessage,
status = HttpStatusCode.UnprocessableEntity,
)
}
}
}

View file

@ -1,6 +1,7 @@
package com.pixelized.server.lwa.server.rest.campaign
import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.characterSheetId
import com.pixelized.shared.lwa.model.campaign.Campaign
import com.pixelized.shared.lwa.protocol.websocket.Message
import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation
@ -10,25 +11,26 @@ import io.ktor.server.response.respondText
fun Engine.putCampaignCharacter(): suspend io.ktor.server.routing.RoutingContext.() -> Unit {
return {
try {
val characterSheetId = call.queryParameters["characterSheetId"]
?: error("missing character sheet id")
// get the query parameter
val characterSheetId = call.queryParameters.characterSheetId
// check if the character is already in the party.
val instanceId = campaignService.campaign().characters.keys
.firstOrNull { key -> key.characterSheetId == characterSheetId }
// handle the error case.
if (instanceId != null) {
error("Character Already in party")
error("Character (characterSheetId:$characterSheetId) Already in party")
}
// create the instance id for the character.
val id = Campaign.CharacterInstance.Id(
prefix = Campaign.CharacterInstance.Id.PLAYER,
characterSheetId = characterSheetId,
instanceId = 0,
)
// add the character to the party.
if (campaignService.addCharacter(id).not()) {
error("Unexpected error occurred when the character instance was added to the party")
}
// API & WebSocket responses.
call.respondText(
text = "Character $characterSheetId successfully added to the party",
status = HttpStatusCode.Accepted,

View file

@ -1,6 +1,7 @@
package com.pixelized.server.lwa.server.rest.campaign
import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.characterSheetId
import com.pixelized.shared.lwa.model.campaign.Campaign
import com.pixelized.shared.lwa.protocol.websocket.Message
import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation
@ -10,9 +11,9 @@ import io.ktor.server.response.respondText
fun Engine.putCampaignNpc(): suspend io.ktor.server.routing.RoutingContext.() -> Unit {
return {
try {
val characterSheetId = call.queryParameters["characterSheetId"]
?: error("missing character sheet id")
// get the query parameter
val characterSheetId = call.queryParameters.characterSheetId
// compute the npc id base on similar character sheets.
val instanceId = campaignService.campaign().npcs.keys
.filter { it.characterSheetId == characterSheetId }
.reduceOrNull { acc, id ->
@ -22,16 +23,17 @@ fun Engine.putCampaignNpc(): suspend io.ktor.server.routing.RoutingContext.() ->
acc
}
}
// create the instance id for the character.
val id = Campaign.CharacterInstance.Id(
prefix = Campaign.CharacterInstance.Id.NPC,
characterSheetId = characterSheetId,
instanceId = instanceId?.let { it.instanceId + 1 } ?: 0,
)
// add the character to the npcs.
if (campaignService.addNpc(id).not()) {
error("Unexpected error occurred when the character instance was added to the npcs")
}
// API & WebSocket responses.
call.respondText(
text = "Character $characterSheetId successfully added to the npcs",
status = HttpStatusCode.Accepted,

View file

@ -1,6 +1,7 @@
package com.pixelized.server.lwa.server.rest.character
import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.characterSheetId
import com.pixelized.shared.lwa.protocol.websocket.Message
import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation
import io.ktor.http.HttpStatusCode
@ -8,10 +9,10 @@ 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
val characterSheetId = call.parameters.characterSheetId
val deleted = characterService.deleteCharacterSheet(characterId = characterSheetId)
if (deleted && id != null) {
if (deleted) {
call.respondText(
text = "${HttpStatusCode.OK}",
status = HttpStatusCode.OK,
@ -19,7 +20,7 @@ fun Engine.deleteCharacter(): suspend io.ktor.server.routing.RoutingContext.() -
webSocket.emit(
Message(
from = "Server",
value = RestSynchronisation.CharacterDelete(characterId = id),
value = RestSynchronisation.CharacterDelete(characterId = characterSheetId),
)
)
} else {

View file

@ -1,15 +1,16 @@
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.server.lwa.utils.extentions.characterSheetId
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)
val id = call.queryParameters.characterSheetId
val body = characterService.characterSheet(id)
if (body != null) {
call.respond(body)
} else {

View file

@ -11,7 +11,7 @@ import io.ktor.server.response.respondText
fun Engine.putCharacter(): suspend io.ktor.server.routing.RoutingContext.() -> Unit {
return {
val form = call.receive<CharacterSheetJson>()
characterService.updateCharacter(
characterService.updateCharacterSheet(
character = form
)
call.respondText(

View file

@ -0,0 +1,20 @@
package com.pixelized.server.lwa.utils.extentions
import com.pixelized.shared.lwa.model.campaign.Campaign
import io.ktor.http.Parameters
val Parameters.characterInstanceId: Campaign.CharacterInstance.Id
get() = Campaign.CharacterInstance.Id(
characterSheetId = characterSheetId,
instanceId = instanceId,
prefix = prefix,
)
val Parameters.characterSheetId
get() = this["characterSheetId"] ?: error("Missing character sheet id.")
val Parameters.instanceId: Int
get() = this["instanceId"]?.toIntOrNull() ?: error("Missing character instance id.")
val Parameters.prefix: Char
get() = this["prefix"]?.get(0) ?: error("Missing character prefix.")