ReModel of the ActiveAlteration system into the Campaign.

This commit is contained in:
Thomas Andres Gomez 2025-03-21 18:06:03 +01:00
parent ecb0a6705f
commit b314a28f82
22 changed files with 254 additions and 317 deletions

View file

@ -1,56 +1,13 @@
package com.pixelized.server.lwa.model.alteration
import com.pixelized.shared.lwa.model.alteration.AlterationJson
import com.pixelized.shared.lwa.model.campaign.Campaign
import com.pixelized.shared.lwa.model.campaign.factory.CampaignJsonFactory
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 AlterationService(
private val store: AlterationStore,
private val campaignJsonFactory: CampaignJsonFactory,
store: AlterationStore,
) {
private val scope = CoroutineScope(Dispatchers.IO + Job())
private val alterations = store.alterationsFlow()
private val actives = store.activeFlow()
.map { data ->
data.mapKeys { it: Map.Entry<String, List<String>> ->
campaignJsonFactory.characterInstanceIdFromJson(characterInstanceIdJson = it.key)
}
}.stateIn(
scope = scope,
started = SharingStarted.Eagerly,
initialValue = emptyMap()
)
private val alterationsFlow = store.alterationsFlow()
fun alterations(): List<AlterationJson> {
return alterations.value
}
fun active(
characterInstanceId: Campaign.CharacterInstance.Id,
): List<String> {
return actives.value[characterInstanceId] ?: emptyList()
}
fun isAlterationActive(
characterInstanceId: Campaign.CharacterInstance.Id,
alterationId: String,
): Boolean {
return actives.value[characterInstanceId]?.contains(alterationId) ?: false
}
suspend fun toggleActiveAlteration(
characterInstanceId: Campaign.CharacterInstance.Id,
alterationId: String,
): Boolean {
return store.toggleActiveAlteration(
characterInstanceId = characterInstanceId,
alterationId = alterationId,
)
return alterationsFlow.value
}
}

View file

@ -121,6 +121,40 @@ class CampaignService(
}
}
suspend fun removeInstance(
characterSheetId: String,
): Boolean {
// fetch all the current campaign character
val characterIds = campaign.characters
.filterKeys { it.characterSheetId == characterSheetId }
.keys
val npcIds = campaign.npcs
.filterKeys { it.characterSheetId == characterSheetId }
.keys
// check if the character is in the campaign.
if (characterIds.isEmpty() && npcIds.isEmpty()) return false
// update the corresponding character
val characters = campaign.characters.toMutableMap()
val npcs = campaign.npcs.toMutableMap()
characterIds.forEach(characters::remove)
npcIds.forEach(npcs::remove)
// save the campaign to the disk + update the flow.
return try {
store.save(
campaign = campaign.copy(
characters = characters,
npcs = npcs,
)
)
true
} catch (exception: Exception) {
false
}
}
suspend fun setScene(
scene: Campaign.Scene,
): Boolean {
@ -142,8 +176,8 @@ class CampaignService(
characteristic: Campaign.CharacterInstance.Characteristic,
value: Int,
) {
when {
campaign.characters.containsKey(characterInstanceId) -> {
when (characterInstanceId.prefix) {
Campaign.CharacterInstance.Id.PLAYER -> {
// fetch all the current campaign character
val characters = campaign.characters.toMutableMap()
// update the corresponding character using the use case.
@ -158,7 +192,7 @@ class CampaignService(
)
}
campaign.npcs.containsKey(characterInstanceId) -> {
Campaign.CharacterInstance.Id.NPC -> {
// fetch all the current campaign character
val npcs = campaign.npcs.toMutableMap()
// update the corresponding character using the use case.
@ -179,8 +213,8 @@ class CampaignService(
characterInstanceId: Campaign.CharacterInstance.Id,
diminished: Int,
) {
when {
campaign.characters.containsKey(characterInstanceId) -> {
when (characterInstanceId.prefix) {
Campaign.CharacterInstance.Id.PLAYER -> {
// fetch all the current campaign character
val characters = campaign.characters.toMutableMap()
// update the corresponding character using the use case.
@ -194,7 +228,7 @@ class CampaignService(
)
}
campaign.npcs.containsKey(characterInstanceId) -> {
Campaign.CharacterInstance.Id.NPC -> {
// fetch all the current campaign character
val npcs = campaign.npcs.toMutableMap()
// update the corresponding character using the use case.
@ -210,6 +244,55 @@ class CampaignService(
}
}
suspend fun toggleAlteration(
characterInstanceId: Campaign.CharacterInstance.Id,
alterationId: String,
) {
when (characterInstanceId.prefix) {
Campaign.CharacterInstance.Id.PLAYER -> {
// fetch all the current campaign character
val characters = campaign.characters.toMutableMap()
// update the corresponding character alterations
characters[characterInstanceId]?.let { character ->
characters[characterInstanceId] = character.copy(
alterations = character.alterations.toMutableList().also { alterations ->
if (alterations.contains(alterationId)) {
alterations.remove(alterationId)
} else {
alterations.add(alterationId)
}
},
)
}
// save the campaign to the disk + update the flow.
store.save(
campaign = campaign.copy(characters = characters)
)
}
Campaign.CharacterInstance.Id.NPC -> {
// fetch all the current campaign character
val characters = campaign.npcs.toMutableMap()
// update the corresponding character alterations
characters[characterInstanceId]?.let { character ->
characters[characterInstanceId] = character.copy(
alterations = character.alterations.toMutableList().also { alterations ->
if (alterations.contains(alterationId)) {
alterations.remove(alterationId)
} else {
alterations.add(alterationId)
}
},
)
}
// save the campaign to the disk + update the flow.
store.save(
campaign = campaign.copy(npcs = characters)
)
}
}
}
suspend fun updateToggleParty() {
store.save(
campaign = campaign.copy(

View file

@ -38,8 +38,8 @@ class CharacterSheetService(
return store.save(sheet = factory.convertFromJson(character))
}
fun deleteCharacterSheet(characterId: String): Boolean {
return store.delete(id = characterId)
fun deleteCharacterSheet(characterSheetId: String): Boolean {
return store.delete(id = characterSheetId)
}
// Data manipulation through WebSocket.

View file

@ -6,12 +6,12 @@ import com.pixelized.server.lwa.model.character.CharacterSheetService
import com.pixelized.shared.lwa.model.campaign.Campaign
import com.pixelized.shared.lwa.model.campaign.factory.CampaignJsonFactory
import com.pixelized.shared.lwa.protocol.websocket.CampaignMessage
import com.pixelized.shared.lwa.protocol.websocket.CampaignMessage.ToggleActiveAlteration
import com.pixelized.shared.lwa.protocol.websocket.GameEvent
import com.pixelized.shared.lwa.protocol.websocket.GameMasterEvent
import com.pixelized.shared.lwa.protocol.websocket.RestSynchronisation
import com.pixelized.shared.lwa.protocol.websocket.RollMessage
import com.pixelized.shared.lwa.protocol.websocket.SocketMessage
import com.pixelized.shared.lwa.protocol.websocket.ToggleActiveAlteration
import com.pixelized.shared.lwa.protocol.websocket.UpdateSkillUsageMessage
import kotlinx.coroutines.flow.MutableSharedFlow
@ -45,6 +45,11 @@ class Engine(
characterInstanceId = instanceId,
diminished = message.diminished,
)
is ToggleActiveAlteration -> campaignService.toggleAlteration(
characterInstanceId = instanceId,
alterationId = message.alterationId,
)
}
}

View file

@ -1,15 +1,14 @@
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.removeCampaignCharacter
import com.pixelized.server.lwa.server.rest.alteration.getAlterations
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.campaign.putCampaignScene
import com.pixelized.server.lwa.server.rest.campaign.putToggleAlteration
import com.pixelized.server.lwa.server.rest.campaign.removeCampaignCharacter
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
@ -86,7 +85,7 @@ class LocalServer {
val frame = Frame.Text(text = data)
try {
send(frame)
} catch (exception : Exception) {
} catch (exception: Exception) {
// TODO
println("WebSocket exception: ${exception.localizedMessage}")
}
@ -112,6 +111,10 @@ class LocalServer {
}
}
)
get(
path = "/alterations",
body = engine.getAlterations(),
)
get(
path = "/characters",
body = engine.getCharacters(),
@ -135,6 +138,10 @@ class LocalServer {
path = "",
body = engine.getCampaign(),
)
put(
path = "/toggleAlteration",
body = engine.putToggleAlteration(),
)
route(path = "/character") {
put(
path = "/update",
@ -160,20 +167,6 @@ class LocalServer {
body = engine.putCampaignScene(),
)
}
route(path = "/alterations") {
get(
path = "",
body = engine.getAlteration(),
)
get(
path = "/active",
body = engine.getActiveAlteration(),
)
put(
path = "/active/toggle",
body = engine.putActiveAlteration(),
)
}
}
}
)

View file

@ -1,25 +0,0 @@
package com.pixelized.server.lwa.server.rest.alteration
import com.pixelized.server.lwa.server.Engine
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 {
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,
)
}
}
}

View file

@ -3,7 +3,7 @@ package com.pixelized.server.lwa.server.rest.alteration
import com.pixelized.server.lwa.server.Engine
import io.ktor.server.response.respond
fun Engine.getAlteration(): suspend io.ktor.server.routing.RoutingContext.() -> Unit {
fun Engine.getAlterations(): suspend io.ktor.server.routing.RoutingContext.() -> Unit {
return {
call.respond(alterationService.alterations())
}

View file

@ -1,13 +1,13 @@
package com.pixelized.server.lwa.server.rest.alteration
package com.pixelized.server.lwa.server.rest.campaign
import com.pixelized.server.lwa.server.Engine
import com.pixelized.server.lwa.utils.extentions.characterInstanceId
import com.pixelized.shared.lwa.protocol.websocket.ToggleActiveAlteration
import com.pixelized.shared.lwa.protocol.websocket.CampaignMessage
import io.ktor.http.HttpStatusCode
import io.ktor.server.request.receive
import io.ktor.server.response.respondText
fun Engine.putActiveAlteration(): suspend io.ktor.server.routing.RoutingContext.() -> Unit {
fun Engine.putToggleAlteration(): suspend io.ktor.server.routing.RoutingContext.() -> Unit {
return {
try {
// get the query parameter
@ -17,30 +17,31 @@ fun Engine.putActiveAlteration(): suspend io.ktor.server.routing.RoutingContext.
val alterationId = call.receive<String>()
// Update the alteration
val updated = alterationService.toggleActiveAlteration(
campaignService.toggleAlteration(
characterInstanceId = characterInstanceId,
alterationId = alterationId,
)
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,
)
val isAlterationActive = campaignService.campaign()
.instances[characterInstanceId]
?.alterations
?.contains(alterationId)
?: false
// share the modification to all client through the websocket.
webSocket.emit(
ToggleActiveAlteration(
CampaignMessage.ToggleActiveAlteration(
timestamp = System.currentTimeMillis(),
prefix = characterInstanceId.prefix,
characterSheetId = characterInstanceId.characterSheetId,
instanceId = characterInstanceId.instanceId,
alterationId = alterationId,
active = alterationService.isAlterationActive(
characterInstanceId = characterInstanceId,
alterationId = alterationId
),
active = isAlterationActive,
)
)
} catch (exception: Exception) {

View file

@ -11,9 +11,10 @@ fun Engine.deleteCharacter(): suspend io.ktor.server.routing.RoutingContext.() -
val characterSheetId = call.parameters.characterSheetId
val deleted = characterService.deleteCharacterSheet(
characterId = characterSheetId
characterSheetId = characterSheetId
) && campaignService.removeInstance(
characterSheetId = characterSheetId,
)
// TODO campaign & alteration cleanup.
if (deleted) {
call.respondText(