diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/DataSyncViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/DataSyncViewModel.kt index 14c3bf4..342e71b 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/DataSyncViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/DataSyncViewModel.kt @@ -6,6 +6,7 @@ import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository import com.pixelized.desktop.lwa.repository.inventory.InventoryRepository import com.pixelized.desktop.lwa.repository.item.ItemRepository +import com.pixelized.desktop.lwa.repository.map.MapRepository import com.pixelized.desktop.lwa.repository.network.NetworkRepository import com.pixelized.desktop.lwa.repository.settings.SettingsRepository import com.pixelized.desktop.lwa.repository.tag.TagRepository @@ -23,6 +24,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach class DataSyncViewModel( + private val mapRepository: MapRepository, private val characterRepository: CharacterSheetRepository, private val inventoryRepository: InventoryRepository, private val alterationRepository: AlterationRepository, @@ -88,6 +90,7 @@ class DataSyncViewModel( campaignRepository.updateCampaign() tagRepository.updateItemTags() itemRepository.updateItemFlow() + mapRepository.updateMapFlow() } catch (exception: Exception) { println(exception.message) // TODO proper exception handling } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/Module.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/Module.kt index 38be68b..937d51c 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/Module.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/Module.kt @@ -12,7 +12,10 @@ import com.pixelized.desktop.lwa.repository.inventory.InventoryRepository import com.pixelized.desktop.lwa.repository.inventory.InventoryStore import com.pixelized.desktop.lwa.repository.item.ItemRepository import com.pixelized.desktop.lwa.repository.item.ItemStore +import com.pixelized.desktop.lwa.repository.map.MapRepository +import com.pixelized.desktop.lwa.repository.map.MapStore import com.pixelized.desktop.lwa.repository.network.NetworkRepository +import com.pixelized.desktop.lwa.repository.resources.ImageResourcesRepository import com.pixelized.desktop.lwa.repository.roll_history.RollHistoryRepository import com.pixelized.desktop.lwa.repository.settings.SettingsFactory import com.pixelized.desktop.lwa.repository.settings.SettingsRepository @@ -65,6 +68,8 @@ import com.pixelized.desktop.lwa.ui.screen.admin.item.edit.GMItemEditFactory import com.pixelized.desktop.lwa.ui.screen.admin.item.edit.GMItemEditViewModel import com.pixelized.desktop.lwa.ui.screen.admin.item.list.GMItemFactory import com.pixelized.desktop.lwa.ui.screen.admin.item.list.GMItemViewModel +import com.pixelized.desktop.lwa.ui.screen.campaign.map.MapSceneFactory +import com.pixelized.desktop.lwa.ui.screen.campaign.map.MapSceneViewModel import com.pixelized.desktop.lwa.ui.screen.levelup.LevelUpFactory import com.pixelized.desktop.lwa.ui.screen.levelup.LevelUpViewModel import com.pixelized.desktop.lwa.ui.screen.settings.SettingsViewModel @@ -125,6 +130,7 @@ val storeDependencies singleOf(::TagStore) singleOf(::ItemStore) singleOf(::InventoryStore) + singleOf(::MapStore) } val repositoryDependencies @@ -138,6 +144,8 @@ val repositoryDependencies singleOf(::TagRepository) singleOf(::ItemRepository) singleOf(::InventoryRepository) + singleOf(::ImageResourcesRepository) + singleOf(::MapRepository) } val factoryDependencies @@ -145,6 +153,7 @@ val factoryDependencies factoryOf(::NetworkFactory) factoryOf(::SettingsFactory) factoryOf(::CampaignJsonFactory) + factoryOf(::MapSceneFactory) factoryOf(::CharacterRibbonFactory) factoryOf(::CharacterDetailHeaderFactory) factoryOf(::CharacterDetailSheetFactory) @@ -175,6 +184,7 @@ val viewModelDependencies viewModelOf(::RollViewModel) viewModelOf(::NetworkViewModel) viewModelOf(::PlayerRibbonViewModel) + viewModelOf(::MapSceneViewModel) viewModelOf(::NpcRibbonViewModel) viewModelOf(::CharacterDetailPanelViewModel) viewModelOf(::CharacterSheetDiminishedDialogViewModel) diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClient.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClient.kt index b595475..c7952c0 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClient.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClient.kt @@ -5,6 +5,7 @@ import com.pixelized.shared.lwa.model.campaign.CampaignJson import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJson import com.pixelized.shared.lwa.model.inventory.InventoryJson import com.pixelized.shared.lwa.model.item.ItemJson +import com.pixelized.shared.lwa.model.map.MapJson import com.pixelized.shared.lwa.model.tag.TagJson import com.pixelized.shared.lwa.protocol.rest.APIResponse import com.pixelized.shared.lwa.protocol.rest.ApiPurseJson @@ -165,6 +166,17 @@ interface LwaClient { suspend fun getItemTags(): APIResponse> + suspend fun getMaps(): APIResponse> + + suspend fun getMap( + mapId: String, + ): APIResponse + + suspend fun putMap( + mapJson: MapJson, + create: Boolean, + ): APIResponse + companion object { fun error(error: APIResponse<*>): Nothing = throw LwaNetworkException(error) } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClientImpl.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClientImpl.kt index 44b73cc..991805b 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClientImpl.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClientImpl.kt @@ -6,6 +6,7 @@ import com.pixelized.shared.lwa.model.campaign.CampaignJson import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJson import com.pixelized.shared.lwa.model.inventory.InventoryJson import com.pixelized.shared.lwa.model.item.ItemJson +import com.pixelized.shared.lwa.model.map.MapJson import com.pixelized.shared.lwa.model.tag.TagJson import com.pixelized.shared.lwa.protocol.rest.APIResponse import com.pixelized.shared.lwa.protocol.rest.ApiPurseJson @@ -282,4 +283,25 @@ class LwaClientImpl( override suspend fun getItemTags(): APIResponse> = client .get("$root/tag/item") .body() + + @Throws + override suspend fun getMaps(): APIResponse> = client + .get("$root/map/all") + .body() + + @Throws + override suspend fun getMap(mapId: String): APIResponse = client + .get("$root/map/detail?mapId=$mapId") + .body() + + @Throws + override suspend fun putMap( + mapJson: MapJson, + create: Boolean, + ): APIResponse = client + .put("$root/map/update") { + contentType(ContentType.Application.Json) + setBody(mapJson) + } + .body>() } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/campaign/CampaignStore.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/campaign/CampaignStore.kt index 3693f8a..72c1fd1 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/campaign/CampaignStore.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/campaign/CampaignStore.kt @@ -3,6 +3,7 @@ package com.pixelized.desktop.lwa.repository.campaign import com.pixelized.desktop.lwa.network.LwaClient import com.pixelized.desktop.lwa.repository.network.NetworkRepository import com.pixelized.shared.lwa.model.campaign.Campaign +import com.pixelized.shared.lwa.model.campaign.Campaign.* import com.pixelized.shared.lwa.model.campaign.factory.CampaignJsonFactory import com.pixelized.shared.lwa.protocol.websocket.CampaignEvent import com.pixelized.shared.lwa.protocol.websocket.GameMasterEvent @@ -141,11 +142,21 @@ class CampaignStore( ) } - is CampaignEvent.UpdateScene -> campaignFlow.update { campaign -> + is CampaignEvent.SceneUpdated -> campaignFlow.update { campaign -> campaign.copy( - scene = Campaign.Scene(name = message.name) + scene = Scene(name = message.name) ) } + + is CampaignEvent.MapUpdated -> campaignFlow.update { campaign -> + campaign.copy( + map = factory.convertFromJson(message.map), + ) + } + + is CampaignEvent.MapDeleted -> campaignFlow.update { campaign -> + campaign.copy(map = null,) + } } } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/map/MapRepository.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/map/MapRepository.kt new file mode 100644 index 0000000..60a9c2d --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/map/MapRepository.kt @@ -0,0 +1,38 @@ +package com.pixelized.desktop.lwa.repository.map + +import com.pixelized.desktop.lwa.repository.campaign.CampaignStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import com.pixelized.shared.lwa.model.map.Map as MiniMap + +class MapRepository( + private val mapStore: MapStore, + private val campaignStore: CampaignStore, +) { + fun mapsFlow(): StateFlow> = mapStore.mapsFlow() + + fun maps() = mapStore.mapsFlow().value + + fun currentMap(): Flow = combine( + campaignStore.campaignFlow(), + mapStore.mapsFlow(), + ) { campaign, maps -> + maps[campaign.map?.id] + } + + suspend fun updateMapFlow() { + mapStore.updateMapFlow() + } + + @Throws + suspend fun updateMap( + map: MiniMap, + create: Boolean, + ) { + mapStore.putMap( + map = map, + create = create, + ) + } +} diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/map/MapStore.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/map/MapStore.kt new file mode 100644 index 0000000..8313659 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/map/MapStore.kt @@ -0,0 +1,107 @@ +package com.pixelized.desktop.lwa.repository.map + +import com.pixelized.desktop.lwa.network.LwaClient +import com.pixelized.desktop.lwa.repository.network.NetworkRepository +import com.pixelized.shared.lwa.model.map.factory.MapJsonFactory +import com.pixelized.shared.lwa.protocol.websocket.ApiSynchronisation +import com.pixelized.shared.lwa.protocol.websocket.SocketMessage +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import com.pixelized.shared.lwa.model.map.Map as MiniMap + +class MapStore( + private val network: NetworkRepository, + private val factory: MapJsonFactory, + private val client: LwaClient, +) { + private val mapsFlow = MutableStateFlow>(emptyMap()) + + init { + val scope = CoroutineScope(Dispatchers.IO + Job()) + // data update through WebSocket. + scope.launch { + network.data.collect(::handleMessage) + } + } + + fun mapsFlow(): StateFlow> = mapsFlow + + suspend fun updateMapFlow() { + val map = try { + getMaps() + } catch (exception: Exception) { + println(exception.message) // TODO proper exception handling + null + } + // guard case if getItem failed + if (map == null) return + // update the flow with the new item. + mapsFlow.update { map } + } + + @Throws + private suspend fun getMaps(): Map { + val request = client.getMaps() + return when (request.success) { + true -> request.data + ?.map { factory.convertFromJson(it) } + ?.associateBy { it.id } + ?: emptyMap() + + else -> LwaClient.error(error = request) + } + } + + @Throws + private suspend fun getMap(id: String): MiniMap? { + val request = client.getMap(mapId = id) + return when (request.success) { + true -> request.data?.let { factory.convertFromJson(it) } + else -> LwaClient.error(error = request) + } + } + + @Throws + suspend fun putMap( + map: MiniMap, + create: Boolean, + ) { + val request = client.putMap( + mapJson = factory.convertToJson(item = map), + create = create, + ) + if (request.success.not()) { + LwaClient.error(error = request) + } + } + + // region: WebSocket & data update. + + private suspend fun handleMessage(message: SocketMessage) { + when (message) { + is ApiSynchronisation.MapApiSynchronisation -> when (message) { + is ApiSynchronisation.MapUpdate -> mapsFlow.update { maps -> + maps.toMutableMap().also { + when (val map = getMap(id = message.id)) { + null -> it.remove(key = message.id) + else -> it[message.id] = map + } + } + } + + is ApiSynchronisation.MapDelete -> mapsFlow.update { maps -> + maps.toMutableMap().also { it.remove(message.id) } + } + } + + else -> Unit + } + } + + // endregion +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/resources/ImageResourcesRepository.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/resources/ImageResourcesRepository.kt new file mode 100644 index 0000000..454f9d9 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/resources/ImageResourcesRepository.kt @@ -0,0 +1,23 @@ +package com.pixelized.desktop.lwa.repository.resources + +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.toComposeImageBitmap +import io.ktor.client.HttpClient +import io.ktor.client.request.get +import io.ktor.client.statement.readRawBytes +import org.jetbrains.skia.Image + +class ImageResourcesRepository( + private val httpClient: HttpClient, +) { + suspend fun load(url: String): ImageBitmap { + try { + val byteArray = httpClient.get(url).readRawBytes() + val skiaImage = Image.makeFromEncoded(byteArray) + return skiaImage.toComposeImageBitmap() + } catch (_: Exception) { + // TODO proper exception handling (error bus ?) + return ImageBitmap(width = 0, height = 0) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/Scene.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/Scene.kt index 841ab95..2c0f495 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/Scene.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/Scene.kt @@ -17,15 +17,10 @@ import com.pixelized.desktop.lwa.ui.composable.scene.drawables.SceneLayer @Stable data class Scene( + val size: IntSize, val layers: List, val elements: List, -) { - val size: IntSize = IntSize( - width = layers.maxOf { it.size.width }, - height = layers.maxOf { it.size.height }, - ) -} - +) @Composable fun Scene( diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/camera/Camera.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/camera/Camera.kt index c52ddfb..800fdee 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/camera/Camera.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/camera/Camera.kt @@ -23,6 +23,7 @@ class Camera( val offset: IntOffset by _offset private var _sceneSize: IntSize by mutableStateOf(IntSize.Zero) + private var _cameraSize: IntSize by mutableStateOf(IntSize.Zero) val cameraSize: IntSize get() = _cameraSize val cameraSizeZoomed: IntSize by derivedStateOf { @@ -40,7 +41,13 @@ class Camera( _sceneSize = sceneSize } - fun handlePanning( + fun move( + offset: IntOffset, + ) { + _offset.value = offset + } + + fun pan( delta: Offset, ) { val value = _offset.value - IntOffset( @@ -50,7 +57,7 @@ class Camera( _offset.value = value } - fun handleZoom( + fun zoom( power: Float, target: IntOffset? = null, ) { diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/camera/Modifier+onCameraControl.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/camera/Modifier+onCameraControl.kt index e3e6034..485d331 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/camera/Modifier+onCameraControl.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/camera/Modifier+onCameraControl.kt @@ -33,7 +33,7 @@ fun Modifier.onCameraControl( event = event, ) { delta -> when { - event.buttons.isTertiaryPressed || (event.keyboardModifiers.isCtrlPressed && event.buttons.isPrimaryPressed) -> camera.handlePanning( + event.buttons.isTertiaryPressed || (event.keyboardModifiers.isCtrlPressed && event.buttons.isPrimaryPressed) -> camera.pan( delta = delta, ) } @@ -41,7 +41,7 @@ fun Modifier.onCameraControl( } .onPointerEvent(PointerEventType.Scroll) { event: PointerEvent -> val change = event.changes.first() - camera.handleZoom( + camera.zoom( power = -change.scrollDelta.y.sign * 0.15f, target = change.position.round(), ) diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/debug/CursorDebugPanel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/debug/CursorDebugPanel.kt index 8a9329d..649fc73 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/debug/CursorDebugPanel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/debug/CursorDebugPanel.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.round import com.pixelized.desktop.lwa.ui.composable.scene.camera.Camera import com.pixelized.desktop.lwa.ui.composable.scene.cursor.Cursor import com.pixelized.desktop.lwa.ui.composable.scene.utils.local @@ -62,7 +63,7 @@ fun CursorDebugPanel( style = MaterialTheme.lwa.typography.debug.propertyValue, text = buildAnnotatedString { withStyle(style.propertyIdSpan) { append("local: ") } - append(cursor.offset.local(camera).toString()) + append(cursor.offset.local(camera).round().toString()) }, ) } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/drawables/SceneDrawable.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/drawables/SceneDrawable.kt index 43c377d..b08a836 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/drawables/SceneDrawable.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/drawables/SceneDrawable.kt @@ -16,7 +16,7 @@ open class SceneDrawable( ) { private val _alpha = Animatable( initialValue = initialAlpha, - typeConverter = Float.Companion.VectorConverter, + typeConverter = Float.VectorConverter, ) val alpha get() = _alpha.value diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/CampaignScreen.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/CampaignScreen.kt index ad42ae4..ddb87c8 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/CampaignScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/CampaignScreen.kt @@ -44,7 +44,6 @@ import com.pixelized.desktop.lwa.ui.composable.character.diminished.CharacterShe import com.pixelized.desktop.lwa.ui.composable.character.diminished.CharacterSheetDiminishedDialogViewModel import com.pixelized.desktop.lwa.ui.composable.error.ErrorSnackHandler import com.pixelized.desktop.lwa.ui.composable.key.KeyHandler -import com.pixelized.desktop.lwa.ui.screen.campaign.map.MapScene import com.pixelized.desktop.lwa.ui.navigation.screen.LocalScreenController import com.pixelized.desktop.lwa.ui.navigation.screen.destination.navigateToLevelScreen import com.pixelized.desktop.lwa.ui.overlay.portrait.PortraitOverlay @@ -94,7 +93,7 @@ fun CampaignScreen( modifier = Modifier.fillMaxSize(), top = { CampaignToolbar( - viewModel = campaignViewModel, + toolBarViewModel = campaignViewModel, ) }, bottom = { diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/chatbox/TextMessageFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/chatbox/TextMessageFactory.kt index 53192a9..04902c2 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/chatbox/TextMessageFactory.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/chatbox/TextMessageFactory.kt @@ -142,9 +142,11 @@ class TextMessageFactory( is CampaignEvent -> when (message) { is CampaignEvent.CharacterAdded -> null is CampaignEvent.CharacterRemoved -> null - is CampaignEvent.UpdateScene -> null is CampaignEvent.NpcAdded -> null is CampaignEvent.NpcRemoved -> null + is CampaignEvent.SceneUpdated -> null + is CampaignEvent.MapUpdated -> null + is CampaignEvent.MapDeleted -> null } is GameMasterEvent -> null diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/map/DahomeMap.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/map/MapScene.kt similarity index 72% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/map/DahomeMap.kt rename to composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/map/MapScene.kt index 6deb722..604d947 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/map/DahomeMap.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/map/MapScene.kt @@ -1,6 +1,8 @@ package com.pixelized.desktop.lwa.ui.screen.campaign.map import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -12,21 +14,19 @@ import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.pixelized.desktop.lwa.ui.composable.scene.Scene -import com.pixelized.desktop.lwa.ui.composable.scene.camera.Camera -import com.pixelized.desktop.lwa.ui.composable.scene.cursor.Cursor import com.pixelized.desktop.lwa.ui.composable.scene.cursor.onCursorControl import com.pixelized.desktop.lwa.ui.composable.scene.debug.CameraDebugPanel import com.pixelized.desktop.lwa.ui.composable.scene.debug.CursorDebugPanel import com.pixelized.desktop.lwa.ui.composable.scene.debug.SceneDebugPanel -import com.pixelized.desktop.lwa.ui.composable.scene.drawables.rememberLayoutFromResource import com.pixelized.desktop.lwa.ui.theme.lwa import kotlinx.coroutines.launch import lwacharactersheet.composeapp.generated.resources.Res @@ -35,46 +35,25 @@ import lwacharactersheet.composeapp.generated.resources.ic_frame_bug_24dp import lwacharactersheet.composeapp.generated.resources.ic_visibility_24dp import lwacharactersheet.composeapp.generated.resources.ic_zoom_in_map_24dp import lwacharactersheet.composeapp.generated.resources.ic_zoom_out_map_24dp -import lwacharactersheet.composeapp.generated.resources.image_dahome_maps -import lwacharactersheet.composeapp.generated.resources.image_dahome_regions import org.jetbrains.compose.resources.painterResource +import org.koin.compose.viewmodel.koinViewModel @Composable fun MapScene( modifier: Modifier = Modifier, - enableDebug: Boolean, + viewModel: MapSceneViewModel = koinViewModel(), ) { val scope = rememberCoroutineScope() - val map = rememberLayoutFromResource( - name = "Dahomé", - resource = Res.drawable.image_dahome_maps, - ) - val mapRegionOverlay = rememberLayoutFromResource( - name = "Région", - resource = Res.drawable.image_dahome_regions, - ) - val camera = remember { - Camera( - initialZoom = 1f, - initialOffset = IntOffset(x = 1407, y = 1520), - ) - } - val cursor = remember { - Cursor() - } - val scene = remember(map, mapRegionOverlay) { - Scene( - layers = listOf(map, mapRegionOverlay), - elements = emptyList(), - ) - } - val openDebugMenu = remember { - mutableStateOf(false) - } + val camera by viewModel.camera.collectAsStateWithLifecycle() + val scene by viewModel.scene.collectAsStateWithLifecycle() + val openDebugMenu = viewModel.displayDebugMenu.collectAsStateWithLifecycle() + val enableDebug = viewModel.enableDebug.collectAsStateWithLifecycle() + val enableLayersToggle = viewModel.enableLayersToggle.collectAsStateWithLifecycle() + Scene( modifier = Modifier - .onCursorControl(camera = camera, cursor = cursor) + .onCursorControl(camera = camera, cursor = viewModel.cursor) .then(other = modifier), scene = scene, camera = camera, @@ -84,11 +63,12 @@ fun MapScene( .align(alignment = Alignment.BottomEnd) .padding(paddingValues = MaterialTheme.lwa.dimen.paddingValues), enableDebug = enableDebug, + enableLayersToggle = enableLayersToggle, onZoomIn = { - camera.handleZoom(power = 0.15f) + camera.zoom(power = 0.15f) }, onZoomOut = { - camera.handleZoom(power = -0.15f) + camera.zoom(power = -0.15f) }, onResetCamera = { camera.resetPosition() @@ -96,14 +76,12 @@ fun MapScene( }, onToggleLayer = { scope.launch { - scene.layers.getOrNull(1)?.let { + scene.layers.drop(1).forEach { it.alpha(alpha = if (it.alpha == 0f) 1f else 0f) } } }, - onToggleDebug = { - openDebugMenu.value = openDebugMenu.value.not() - }, + onToggleDebug = viewModel::toggleDebugMenu, ) AnimatedVisibility( @@ -128,7 +106,7 @@ fun MapScene( ) CursorDebugPanel( camera = camera, - cursors = remember { listOf(cursor) }, + cursors = remember { listOf(viewModel.cursor) }, ) } } @@ -139,7 +117,8 @@ fun MapScene( @Composable private fun MapActions( modifier: Modifier = Modifier, - enableDebug: Boolean, + enableDebug: State, + enableLayersToggle: State, onZoomIn: () -> Unit, onZoomOut: () -> Unit, onResetCamera: () -> Unit, @@ -173,16 +152,24 @@ private fun MapActions( contentDescription = null ) } - IconButton( - onClick = onToggleLayer, + AnimatedVisibility( + visible = enableLayersToggle.value, + enter = fadeIn(), + exit = fadeOut(), ) { - Icon( - painter = painterResource(Res.drawable.ic_visibility_24dp), - contentDescription = null - ) + IconButton( + onClick = onToggleLayer, + ) { + Icon( + painter = painterResource(Res.drawable.ic_visibility_24dp), + contentDescription = null + ) + } } AnimatedVisibility( - visible = enableDebug, + visible = enableDebug.value, + enter = fadeIn(), + exit = fadeOut(), ) { IconButton( onClick = onToggleDebug, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/map/MapSceneFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/map/MapSceneFactory.kt new file mode 100644 index 0000000..4500f97 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/map/MapSceneFactory.kt @@ -0,0 +1,44 @@ +package com.pixelized.desktop.lwa.ui.screen.campaign.map + +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize +import com.pixelized.desktop.lwa.ui.composable.scene.Scene +import com.pixelized.desktop.lwa.ui.composable.scene.camera.Camera +import com.pixelized.desktop.lwa.ui.composable.scene.drawables.SceneLayer +import com.pixelized.shared.lwa.model.map.Map + +class MapSceneFactory { + + suspend fun convertToScene( + map: Map, + image: suspend (String) -> ImageBitmap, + ): Scene { + return Scene( + size = IntSize( + width = map.size.width, + height = map.size.height, + ), + layers = map.resources.map { resource -> + SceneLayer( + id = resource.id, + name = resource.name, + texture = image(resource.uri), + ) + }, + elements = emptyList(), + ) + } + + fun convertToCamera( + camera: Map.Camera + ) : Camera { + return Camera( + initialZoom = camera.zoom, + initialOffset = IntOffset( + x = camera.offsetX, + y = camera.offsetY, + ) + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/map/MapSceneViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/map/MapSceneViewModel.kt new file mode 100644 index 0000000..6a47bf8 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/map/MapSceneViewModel.kt @@ -0,0 +1,96 @@ +package com.pixelized.desktop.lwa.ui.screen.campaign.map + +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository +import com.pixelized.desktop.lwa.repository.map.MapRepository +import com.pixelized.desktop.lwa.repository.resources.ImageResourcesRepository +import com.pixelized.desktop.lwa.repository.settings.SettingsRepository +import com.pixelized.desktop.lwa.ui.composable.scene.Scene +import com.pixelized.desktop.lwa.ui.composable.scene.camera.Camera +import com.pixelized.desktop.lwa.ui.composable.scene.cursor.Cursor +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn + +class MapSceneViewModel( + resourcesRepository: ImageResourcesRepository, + settingsRepository: SettingsRepository, + campaignRepository: CampaignRepository, + mapRepository: MapRepository, + factory: MapSceneFactory, +) : ViewModel() { + + private val _displayDebugMenu = MutableStateFlow(false) + val displayDebugMenu: StateFlow get() = _displayDebugMenu + + val enableDebug = settingsRepository.settingsFlow() + .map { it.isGameMaster ?: false } + .stateIn( + scope = viewModelScope, + started = SharingStarted.Lazily, + initialValue = false, + ) + + private val _camera = MutableStateFlow(defaultCamera()) + val camera: StateFlow = _camera + + private val _scene = MutableStateFlow(defaultScene()) + val scene: StateFlow = _scene + + val enableLayersToggle: StateFlow = scene + .map { it.layers.size > 1 } + .stateIn( + scope = viewModelScope, + started = SharingStarted.Lazily, + initialValue = false, + ) + + val cursor = Cursor() + + init { + combine( + campaignRepository.campaignFlow(), + mapRepository.currentMap(), + ) { campaign, map -> + when (map) { + null -> null + else -> (campaign.map?.camera ?: map.camera) to map + } + } + .mapNotNull { it } + .onEach { (camera, map) -> + _scene.value = factory.convertToScene( + map = map, + image = { resourcesRepository.load(it) }, + ) + _camera.value = factory.convertToCamera( + camera = camera + ) + } + .launchIn(scope = viewModelScope) + } + + fun toggleDebugMenu() { + _displayDebugMenu.value = _displayDebugMenu.value.not() + } + + private fun defaultCamera() = Camera( + initialZoom = 1f, + initialOffset = IntOffset.Zero, + ) + + private fun defaultScene() = Scene( + size = IntSize(width = 3840, height = 2160), + layers = emptyList(), + elements = emptyList() + ) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/toolbar/CampaignToolbar.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/toolbar/CampaignToolbar.kt index fc8ceca..05a9c6f 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/toolbar/CampaignToolbar.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/toolbar/CampaignToolbar.kt @@ -26,6 +26,7 @@ import com.pixelized.desktop.lwa.ui.navigation.screen.LocalScreenController import com.pixelized.desktop.lwa.ui.navigation.screen.destination.navigateToSettings import com.pixelized.desktop.lwa.ui.navigation.window.destination.openGameMasterWindow import com.pixelized.desktop.lwa.ui.screen.campaign.map.MapScene +import com.pixelized.desktop.lwa.ui.screen.campaign.map.MapSceneViewModel import com.pixelized.desktop.lwa.ui.screen.campaign.toolbar.links.ResourcesMenu import com.pixelized.desktop.lwa.ui.screen.campaign.toolbar.network.NetworkMenu import com.pixelized.desktop.lwa.ui.theme.lwa @@ -49,20 +50,23 @@ data class CampaignMenuStateUio( @Composable fun CampaignToolbar( - viewModel: CampaignToolbarViewModel = koinViewModel(), + toolBarViewModel: CampaignToolbarViewModel = koinViewModel(), + mapViewModel: MapSceneViewModel = koinViewModel(), ) { val windows = LocalWindowController.current val screen = LocalScreenController.current - val title = viewModel.title.collectAsStateWithLifecycle() - val status = viewModel.status.collectAsStateWithLifecycle() - val isAdmin = viewModel.isAdmin.collectAsStateWithLifecycle() - val menusState = viewModel.menusState.collectAsStateWithLifecycle() + val title = toolBarViewModel.title.collectAsStateWithLifecycle() + val status = toolBarViewModel.status.collectAsStateWithLifecycle() + val isAdmin = toolBarViewModel.isAdmin.collectAsStateWithLifecycle() + val isMapEnable = toolBarViewModel.isMapEnable.collectAsStateWithLifecycle() + val menusState = toolBarViewModel.menusState.collectAsStateWithLifecycle() CampaignToolbarContent( title = title, status = status, isAdmin = isAdmin, + displayMapButton = isMapEnable, menusState = menusState, onAdmin = { windows.openGameMasterWindow() @@ -70,12 +74,18 @@ fun CampaignToolbar( onSettings = { screen.navigateToSettings() }, - onNetwork = viewModel::onNetwork, - onResources = viewModel::onResources, - onMap = viewModel::onMap, - onDismissNetworkMenu = viewModel::onDismissNetworkMenu, - onDismissResourcesMenu = viewModel::onDismissResourcesMenu, - onDismissMapMenu = viewModel::onDismissMapMenu, + onNetwork = toolBarViewModel::onNetwork, + onResources = toolBarViewModel::onResources, + onMap = toolBarViewModel::onMap, + onDismissNetworkMenu = toolBarViewModel::onDismissNetworkMenu, + onDismissResourcesMenu = toolBarViewModel::onDismissResourcesMenu, + onDismissMapMenu = toolBarViewModel::onDismissMapMenu, + map = { + MapScene( + modifier = Modifier.size(640.dp), + viewModel = mapViewModel, + ) + } ) } @@ -86,6 +96,7 @@ private fun CampaignToolbarContent( title: State, status: State, isAdmin: State, + displayMapButton: State, menusState: State, onAdmin: () -> Unit, onNetwork: () -> Unit, @@ -95,6 +106,7 @@ private fun CampaignToolbarContent( onDismissNetworkMenu: () -> Unit, onDismissResourcesMenu: () -> Unit, onDismissMapMenu: () -> Unit, + map: @Composable () -> Unit, ) { TopAppBar( modifier = modifier, @@ -118,25 +130,27 @@ private fun CampaignToolbarContent( ) } } - IconButton( - onClick = onMap, + AnimatedVisibility( + visible = displayMapButton.value, + enter = fadeIn(), + exit = fadeOut(), ) { - Icon( - painter = painterResource(Res.drawable.ic_map_24dp), - tint = MaterialTheme.colors.primary, - contentDescription = null, - ) - DropdownMenu( - expanded = menusState.value.isMapMenuOpen, - onDismissRequest = onDismissMapMenu, - content = { - MapScene( - modifier = Modifier.size(640.dp), - enableDebug = isAdmin.value, - ) - }, - ) + IconButton( + onClick = onMap, + ) { + Icon( + painter = painterResource(Res.drawable.ic_map_24dp), + tint = MaterialTheme.colors.primary, + contentDescription = null, + ) + DropdownMenu( + expanded = menusState.value.isMapMenuOpen, + onDismissRequest = onDismissMapMenu, + content = { map() }, + ) + } } + IconButton( onClick = onResources, ) { diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/toolbar/CampaignToolbarViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/toolbar/CampaignToolbarViewModel.kt index fbc3b7b..76e4f1a 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/toolbar/CampaignToolbarViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/toolbar/CampaignToolbarViewModel.kt @@ -3,6 +3,7 @@ package com.pixelized.desktop.lwa.ui.screen.campaign.toolbar import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository +import com.pixelized.desktop.lwa.repository.map.MapRepository import com.pixelized.desktop.lwa.repository.network.NetworkRepository import com.pixelized.desktop.lwa.repository.settings.SettingsRepository import kotlinx.coroutines.flow.MutableStateFlow @@ -14,6 +15,7 @@ import kotlinx.coroutines.flow.update class CampaignToolbarViewModel( campaignRepository: CampaignRepository, + mapRepository: MapRepository, networkRepository: NetworkRepository, settingsRepository: SettingsRepository, ) : ViewModel() { @@ -45,6 +47,14 @@ class CampaignToolbarViewModel( initialValue = false, ) + val isMapEnable = mapRepository.currentMap() + .map { it != null } + .stateIn( + scope = viewModelScope, + started = SharingStarted.Lazily, + initialValue = false, + ) + val isGameMaster = settingsRepository.settingsFlow() .map { it.isGameMaster ?: false } .stateIn( diff --git a/server/src/main/kotlin/Module.kt b/server/src/main/kotlin/Module.kt index 9e4443a..31219a6 100644 --- a/server/src/main/kotlin/Module.kt +++ b/server/src/main/kotlin/Module.kt @@ -1,17 +1,19 @@ import com.pixelized.server.lwa.logics.ItemUsageLogic -import com.pixelized.server.lwa.model.alteration.AlterationService -import com.pixelized.server.lwa.model.alteration.AlterationStore -import com.pixelized.server.lwa.model.campaign.CampaignService -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.model.inventory.InventoryService -import com.pixelized.server.lwa.model.inventory.InventoryStore -import com.pixelized.server.lwa.model.item.ItemService -import com.pixelized.server.lwa.model.item.ItemStore -import com.pixelized.server.lwa.model.tag.TagService -import com.pixelized.server.lwa.model.tag.TagStore +import com.pixelized.server.lwa.services.alteration.AlterationService +import com.pixelized.server.lwa.services.alteration.AlterationStore +import com.pixelized.server.lwa.services.campaign.CampaignService +import com.pixelized.server.lwa.services.campaign.CampaignStore +import com.pixelized.server.lwa.services.character.CharacterSheetService +import com.pixelized.server.lwa.services.character.CharacterSheetStore +import com.pixelized.server.lwa.services.inventory.InventoryService +import com.pixelized.server.lwa.services.inventory.InventoryStore +import com.pixelized.server.lwa.services.item.ItemService +import com.pixelized.server.lwa.services.item.ItemStore +import com.pixelized.server.lwa.services.tag.TagService +import com.pixelized.server.lwa.services.tag.TagStore import com.pixelized.server.lwa.server.Engine +import com.pixelized.server.lwa.services.map.MapService +import com.pixelized.server.lwa.services.map.MapStore import com.pixelized.shared.lwa.utils.PathProvider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -51,6 +53,7 @@ val storeDependencies singleOf(::InventoryStore) singleOf(::ItemStore) singleOf(::TagStore) + singleOf(::MapStore) } val serviceDependencies @@ -61,6 +64,7 @@ val serviceDependencies singleOf(::InventoryService) singleOf(::ItemService) singleOf(::TagService) + singleOf(::MapService) } val logicsDependencies diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/logics/ItemUsageLogic.kt b/server/src/main/kotlin/com/pixelized/server/lwa/logics/ItemUsageLogic.kt index 094958e..c9f3268 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/logics/ItemUsageLogic.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/logics/ItemUsageLogic.kt @@ -1,7 +1,7 @@ package com.pixelized.server.lwa.logics -import com.pixelized.server.lwa.model.character.CharacterSheetService -import com.pixelized.server.lwa.model.inventory.InventoryService +import com.pixelized.server.lwa.services.character.CharacterSheetService +import com.pixelized.server.lwa.services.inventory.InventoryService class ItemUsageLogic( private val characterSheetService: CharacterSheetService, 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 88f90d0..a34b884 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 @@ -1,18 +1,20 @@ package com.pixelized.server.lwa.server import com.pixelized.server.lwa.logics.ItemUsageLogic -import com.pixelized.server.lwa.model.alteration.AlterationService -import com.pixelized.server.lwa.model.alteration.AlterationStore -import com.pixelized.server.lwa.model.campaign.CampaignService -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.model.inventory.InventoryService -import com.pixelized.server.lwa.model.inventory.InventoryStore -import com.pixelized.server.lwa.model.item.ItemService -import com.pixelized.server.lwa.model.item.ItemStore -import com.pixelized.server.lwa.model.tag.TagService -import com.pixelized.server.lwa.model.tag.TagStore +import com.pixelized.server.lwa.services.alteration.AlterationService +import com.pixelized.server.lwa.services.alteration.AlterationStore +import com.pixelized.server.lwa.services.campaign.CampaignService +import com.pixelized.server.lwa.services.campaign.CampaignStore +import com.pixelized.server.lwa.services.character.CharacterSheetService +import com.pixelized.server.lwa.services.character.CharacterSheetStore +import com.pixelized.server.lwa.services.inventory.InventoryService +import com.pixelized.server.lwa.services.inventory.InventoryStore +import com.pixelized.server.lwa.services.item.ItemService +import com.pixelized.server.lwa.services.item.ItemStore +import com.pixelized.server.lwa.services.map.MapService +import com.pixelized.server.lwa.services.map.MapStore +import com.pixelized.server.lwa.services.tag.TagService +import com.pixelized.server.lwa.services.tag.TagStore import com.pixelized.shared.lwa.model.campaign.factory.CampaignJsonFactory import com.pixelized.shared.lwa.protocol.websocket.ApiSynchronisation import com.pixelized.shared.lwa.protocol.websocket.CampaignEvent @@ -30,6 +32,7 @@ class Engine( val itemService: ItemService, val inventoryService: InventoryService, val tagService: TagService, + val mapService: MapService, val campaignJsonFactory: CampaignJsonFactory, val itemUsageLogic: ItemUsageLogic, private val campaignStore: CampaignStore, @@ -38,6 +41,7 @@ class Engine( private val itemStore: ItemStore, private val inventoryStore: InventoryStore, private val tagStore: TagStore, + private val mapStore: MapStore, ) { val webSocket = MutableSharedFlow() @@ -106,7 +110,19 @@ class Engine( characterSheetId = message.characterSheetId, ) - is CampaignEvent.UpdateScene -> Unit + is CampaignEvent.MapUpdated -> { + // convert the map into the a usable data model. + val map = campaignJsonFactory.convertFromJson(json = message.map) + // update the map + campaignService.setMap(map = map) + } + + is CampaignEvent.MapDeleted -> { + // update the map + campaignService.setMap(map = null) + } + + is CampaignEvent.SceneUpdated -> Unit } is GameAdminEvent -> when (message) { @@ -117,6 +133,7 @@ class Engine( itemStore.updateItemsFlow() inventoryStore.updateInventoryFlow() tagStore.updateTagFlow() + mapStore.updateMapFlow() webSocket.emit( value = GameAdminEvent.ServerSynchronization( 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 9441308..9bd0813 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 @@ -5,7 +5,9 @@ import com.pixelized.server.lwa.server.rest.alteration.deleteAlteration import com.pixelized.server.lwa.server.rest.alteration.getAlteration import com.pixelized.server.lwa.server.rest.alteration.getAlterations import com.pixelized.server.lwa.server.rest.alteration.putAlteration +import com.pixelized.server.lwa.server.rest.campaign.deleteCampaignMap import com.pixelized.server.lwa.server.rest.campaign.getCampaign +import com.pixelized.server.lwa.server.rest.campaign.putCampaignMap 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 @@ -23,9 +25,9 @@ import com.pixelized.server.lwa.server.rest.character.putCharacterFatigue import com.pixelized.server.lwa.server.rest.inventory.changeInventoryItemCount import com.pixelized.server.lwa.server.rest.inventory.consumeInventoryItem import com.pixelized.server.lwa.server.rest.inventory.createInventoryItem -import com.pixelized.server.lwa.server.rest.inventory.equipInventoryItem import com.pixelized.server.lwa.server.rest.inventory.deleteInventory import com.pixelized.server.lwa.server.rest.inventory.deleteInventoryItem +import com.pixelized.server.lwa.server.rest.inventory.equipInventoryItem import com.pixelized.server.lwa.server.rest.inventory.getInventory import com.pixelized.server.lwa.server.rest.inventory.putInventory import com.pixelized.server.lwa.server.rest.inventory.putPurse @@ -33,6 +35,10 @@ import com.pixelized.server.lwa.server.rest.item.deleteItem import com.pixelized.server.lwa.server.rest.item.getItem import com.pixelized.server.lwa.server.rest.item.getItems import com.pixelized.server.lwa.server.rest.item.putItem +import com.pixelized.server.lwa.server.rest.map.deleteMap +import com.pixelized.server.lwa.server.rest.map.getMap +import com.pixelized.server.lwa.server.rest.map.getMaps +import com.pixelized.server.lwa.server.rest.map.putMap import com.pixelized.server.lwa.server.rest.tag.getAlterationTags import com.pixelized.server.lwa.server.rest.tag.getCharacterTags import com.pixelized.server.lwa.server.rest.tag.getItemTags @@ -163,6 +169,18 @@ class LocalServer { path = "/scene", body = engine.putCampaignScene(), ) + route( + path = "/map", + ) { + put( + path = "/update", + body = engine.putCampaignMap(), + ) + put( + path = "/delete", + body = engine.deleteCampaignMap(), + ) + } } route( path = "/character", @@ -306,6 +324,24 @@ class LocalServer { ) } } + route(path = "/map") { + get( + path = "/all", + body = engine.getMaps(), + ) + get( + path = "/detail", + body = engine.getMap(), + ) + put( + path = "/update", + body = engine.putMap(), + ) + delete( + path = "/delete", + body = engine.deleteMap(), + ) + } } } ) diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/alteration/poeut.json b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/alteration/poeut.json new file mode 100644 index 0000000..cda8cbe --- /dev/null +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/alteration/poeut.json @@ -0,0 +1,25 @@ +{ + "id": "DAHOME_432_PU", + "name": "Dahomé", + "camera": { + "zoom": 1.52, + "offsetX": 1594, + "offsetY": 1149 + }, + "size": { + "width": 3840, + "height": 2160 + }, + "resources": [ + { + "id": "ROOT", + "name": "Root", + "uri": "https://i.ibb.co/7dvDJ50L/image-dahome-maps.webp" + }, + { + "id": "REGION", + "name": "Frontières", + "uri": "https://i.ibb.co/mC4jw8Yj/image-dahome-regions.webp" + } + ] +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/DELETE_Campaign_Map.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/DELETE_Campaign_Map.kt new file mode 100644 index 0000000..25f87d1 --- /dev/null +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/DELETE_Campaign_Map.kt @@ -0,0 +1,32 @@ +package com.pixelized.server.lwa.server.rest.campaign + +import com.pixelized.server.lwa.server.Engine +import com.pixelized.server.lwa.utils.extentions.exception +import com.pixelized.shared.lwa.protocol.rest.APIResponse +import com.pixelized.shared.lwa.protocol.websocket.CampaignEvent +import io.ktor.server.response.respond +import io.ktor.server.routing.RoutingContext + +fun Engine.deleteCampaignMap(): suspend RoutingContext.() -> Unit { + return { + try { + // update the campaign. + campaignService.setMap( + map = null, + ) + // API & WebSocket responses + call.respond( + message = APIResponse.success(), + ) + webSocket.emit( + value = CampaignEvent.MapDeleted( + timestamp = System.currentTimeMillis(), + ) + ) + } catch (exception: Exception) { + call.exception( + exception = exception + ) + } + } +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/PUT_Campaign_Map.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/PUT_Campaign_Map.kt new file mode 100644 index 0000000..dd17a2d --- /dev/null +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/PUT_Campaign_Map.kt @@ -0,0 +1,39 @@ +package com.pixelized.server.lwa.server.rest.campaign + +import com.pixelized.server.lwa.server.Engine +import com.pixelized.server.lwa.utils.extentions.exception +import com.pixelized.shared.lwa.model.campaign.CampaignJsonV2 +import com.pixelized.shared.lwa.protocol.rest.APIResponse +import com.pixelized.shared.lwa.protocol.websocket.CampaignEvent +import io.ktor.server.request.receive +import io.ktor.server.response.respond +import io.ktor.server.routing.RoutingContext + +fun Engine.putCampaignMap(): suspend RoutingContext.() -> Unit { + return { + try { + // Get the map json from the body of the request + val form = call.receive() + // convert the map into the a usable data model. + val map = campaignJsonFactory.convertFromJson(json = form) + // update the campaign. + campaignService.setMap( + map = map, + ) + // API & WebSocket responses + call.respond( + message = APIResponse.success(), + ) + webSocket.emit( + value = CampaignEvent.MapUpdated( + timestamp = System.currentTimeMillis(), + map = form, + ) + ) + } catch (exception: Exception) { + call.exception( + exception = exception + ) + } + } +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/PUT_Campaign_Scene_Name.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/PUT_Campaign_Scene_Name.kt index 2da6626..8ad2b9b 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/PUT_Campaign_Scene_Name.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/campaign/PUT_Campaign_Scene_Name.kt @@ -25,7 +25,7 @@ fun Engine.putCampaignScene(): suspend RoutingContext.() -> Unit { message = APIResponse.success(), ) webSocket.emit( - value = CampaignEvent.UpdateScene( + value = CampaignEvent.SceneUpdated( timestamp = System.currentTimeMillis(), name = scene.name, ) diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/map/DELETE_Map.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/map/DELETE_Map.kt new file mode 100644 index 0000000..eba5746 --- /dev/null +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/map/DELETE_Map.kt @@ -0,0 +1,36 @@ +package com.pixelized.server.lwa.server.rest.map + +import com.pixelized.server.lwa.server.Engine +import com.pixelized.server.lwa.utils.extentions.exception +import com.pixelized.server.lwa.utils.extentions.mapId +import com.pixelized.shared.lwa.protocol.rest.APIResponse +import com.pixelized.shared.lwa.protocol.websocket.ApiSynchronisation +import io.ktor.server.response.respond +import io.ktor.server.routing.RoutingContext + +fun Engine.deleteMap(): suspend RoutingContext.() -> Unit { + return { + try { + // get the query parameter + val mapId = call.queryParameters.mapId + // delete the map. + mapService.delete( + mapId = mapId, + ) + // API & WebSocket responses. + call.respond( + message = APIResponse.success(), + ) + webSocket.emit( + value = ApiSynchronisation.MapDelete( + timestamp = System.currentTimeMillis(), + id = mapId, + ), + ) + } catch (exception: Exception) { + call.exception( + exception = exception, + ) + } + } +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/map/GET_Map.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/map/GET_Map.kt new file mode 100644 index 0000000..dd16110 --- /dev/null +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/map/GET_Map.kt @@ -0,0 +1,29 @@ +package com.pixelized.server.lwa.server.rest.map + +import com.pixelized.server.lwa.server.Engine +import com.pixelized.server.lwa.utils.extentions.exception +import com.pixelized.server.lwa.utils.extentions.mapId +import com.pixelized.shared.lwa.protocol.rest.APIResponse +import io.ktor.server.response.respond +import io.ktor.server.routing.RoutingContext + +fun Engine.getMap(): suspend RoutingContext.() -> Unit { + return { + try { + // get the query parameter + val mapId = call.queryParameters.mapId + // fetch the map + val map = mapService.map(id = mapId) + // respond to the request + call.respond( + message = APIResponse.success( + data = map, + ), + ) + } catch (exception: Exception) { + call.exception( + exception = exception + ) + } + } +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/map/GET_Maps.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/map/GET_Maps.kt new file mode 100644 index 0000000..a88b8cc --- /dev/null +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/map/GET_Maps.kt @@ -0,0 +1,24 @@ +package com.pixelized.server.lwa.server.rest.map + +import com.pixelized.server.lwa.server.Engine +import com.pixelized.server.lwa.utils.extentions.exception +import com.pixelized.shared.lwa.protocol.rest.APIResponse +import com.pixelized.shared.lwa.protocol.websocket.ApiSynchronisation +import io.ktor.server.response.respond +import io.ktor.server.routing.RoutingContext + +fun Engine.getMaps(): suspend RoutingContext.() -> Unit { + return { + try { + call.respond( + message = APIResponse.success( + data = mapService.maps(), + ), + ) + } catch (exception: Exception) { + call.exception( + exception = exception + ) + } + } +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/map/PUT_Map.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/map/PUT_Map.kt new file mode 100644 index 0000000..bbfee37 --- /dev/null +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/map/PUT_Map.kt @@ -0,0 +1,37 @@ +package com.pixelized.server.lwa.server.rest.map + +import com.pixelized.server.lwa.server.Engine +import com.pixelized.server.lwa.utils.extentions.create +import com.pixelized.server.lwa.utils.extentions.exception +import com.pixelized.shared.lwa.model.alteration.AlterationJson +import com.pixelized.shared.lwa.model.map.MapJson +import com.pixelized.shared.lwa.protocol.rest.APIResponse +import com.pixelized.shared.lwa.protocol.websocket.ApiSynchronisation +import io.ktor.server.request.receive +import io.ktor.server.response.respond +import io.ktor.server.routing.RoutingContext + +fun Engine.putMap(): suspend RoutingContext.() -> Unit { + return { + try { + val form = call.receive() + + mapService.save( + json = form, + ) + call.respond( + message = APIResponse.success(), + ) + webSocket.emit( + value = ApiSynchronisation.MapUpdate( + timestamp = System.currentTimeMillis(), + id = form.id, + ), + ) + } catch (exception: Exception) { + call.exception( + exception = exception, + ) + } + } +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/alteration/AlterationService.kt b/server/src/main/kotlin/com/pixelized/server/lwa/services/alteration/AlterationService.kt similarity index 96% rename from server/src/main/kotlin/com/pixelized/server/lwa/model/alteration/AlterationService.kt rename to server/src/main/kotlin/com/pixelized/server/lwa/services/alteration/AlterationService.kt index 91bb091..90725c8 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/alteration/AlterationService.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/services/alteration/AlterationService.kt @@ -1,4 +1,4 @@ -package com.pixelized.server.lwa.model.alteration +package com.pixelized.server.lwa.services.alteration import com.pixelized.shared.lwa.model.alteration.AlterationJson import com.pixelized.shared.lwa.model.alteration.AlterationJsonFactory diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/alteration/AlterationStore.kt b/server/src/main/kotlin/com/pixelized/server/lwa/services/alteration/AlterationStore.kt similarity index 99% rename from server/src/main/kotlin/com/pixelized/server/lwa/model/alteration/AlterationStore.kt rename to server/src/main/kotlin/com/pixelized/server/lwa/services/alteration/AlterationStore.kt index 2414213..0bbde15 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/alteration/AlterationStore.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/services/alteration/AlterationStore.kt @@ -1,4 +1,4 @@ -package com.pixelized.server.lwa.model.alteration +package com.pixelized.server.lwa.services.alteration import com.pixelized.server.lwa.server.exception.BusinessException import com.pixelized.server.lwa.server.exception.FileReadException diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/campaign/CampaignService.kt b/server/src/main/kotlin/com/pixelized/server/lwa/services/campaign/CampaignService.kt similarity index 94% rename from server/src/main/kotlin/com/pixelized/server/lwa/model/campaign/CampaignService.kt rename to server/src/main/kotlin/com/pixelized/server/lwa/services/campaign/CampaignService.kt index 7521cac..3500d6b 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/campaign/CampaignService.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/services/campaign/CampaignService.kt @@ -1,4 +1,4 @@ -package com.pixelized.server.lwa.model.campaign +package com.pixelized.server.lwa.services.campaign import com.pixelized.server.lwa.server.exception.BusinessException import com.pixelized.shared.lwa.model.campaign.Campaign @@ -120,6 +120,16 @@ class CampaignService( ) } + @Throws + suspend fun setMap( + map: Campaign.MiniMap?, + ) { + // save the campaign to the disk + update the flow. + store.save( + campaign = campaign.copy(map = map) + ) + } + // Data manipulation through WebSocket. suspend fun updateToggleParty() { diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/campaign/CampaignStore.kt b/server/src/main/kotlin/com/pixelized/server/lwa/services/campaign/CampaignStore.kt similarity index 98% rename from server/src/main/kotlin/com/pixelized/server/lwa/model/campaign/CampaignStore.kt rename to server/src/main/kotlin/com/pixelized/server/lwa/services/campaign/CampaignStore.kt index 63bc43d..1749730 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/campaign/CampaignStore.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/services/campaign/CampaignStore.kt @@ -1,4 +1,4 @@ -package com.pixelized.server.lwa.model.campaign +package com.pixelized.server.lwa.services.campaign import com.pixelized.server.lwa.server.exception.FileReadException import com.pixelized.server.lwa.server.exception.FileWriteException @@ -10,7 +10,6 @@ import com.pixelized.shared.lwa.model.campaign.factory.CampaignJsonFactory import com.pixelized.shared.lwa.utils.PathProvider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/character/CharacterSheetService.kt b/server/src/main/kotlin/com/pixelized/server/lwa/services/character/CharacterSheetService.kt similarity index 98% rename from server/src/main/kotlin/com/pixelized/server/lwa/model/character/CharacterSheetService.kt rename to server/src/main/kotlin/com/pixelized/server/lwa/services/character/CharacterSheetService.kt index a02611e..b33481c 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/character/CharacterSheetService.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/services/character/CharacterSheetService.kt @@ -1,4 +1,4 @@ -package com.pixelized.server.lwa.model.character +package com.pixelized.server.lwa.services.character import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJson diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/character/CharacterSheetStore.kt b/server/src/main/kotlin/com/pixelized/server/lwa/services/character/CharacterSheetStore.kt similarity index 99% rename from server/src/main/kotlin/com/pixelized/server/lwa/model/character/CharacterSheetStore.kt rename to server/src/main/kotlin/com/pixelized/server/lwa/services/character/CharacterSheetStore.kt index 3cd0b3d..0d00eed 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/character/CharacterSheetStore.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/services/character/CharacterSheetStore.kt @@ -1,4 +1,4 @@ -package com.pixelized.server.lwa.model.character +package com.pixelized.server.lwa.services.character import com.pixelized.server.lwa.server.exception.BusinessException import com.pixelized.server.lwa.server.exception.FileReadException diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/inventory/InventoryService.kt b/server/src/main/kotlin/com/pixelized/server/lwa/services/inventory/InventoryService.kt similarity index 98% rename from server/src/main/kotlin/com/pixelized/server/lwa/model/inventory/InventoryService.kt rename to server/src/main/kotlin/com/pixelized/server/lwa/services/inventory/InventoryService.kt index e9538af..b481fdf 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/inventory/InventoryService.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/services/inventory/InventoryService.kt @@ -1,6 +1,6 @@ -package com.pixelized.server.lwa.model.inventory +package com.pixelized.server.lwa.services.inventory -import com.pixelized.server.lwa.model.item.ItemStore +import com.pixelized.server.lwa.services.item.ItemStore import com.pixelized.server.lwa.server.exception.BusinessException import com.pixelized.shared.lwa.model.inventory.Inventory import com.pixelized.shared.lwa.model.inventory.InventoryJson diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/inventory/InventoryStore.kt b/server/src/main/kotlin/com/pixelized/server/lwa/services/inventory/InventoryStore.kt similarity index 99% rename from server/src/main/kotlin/com/pixelized/server/lwa/model/inventory/InventoryStore.kt rename to server/src/main/kotlin/com/pixelized/server/lwa/services/inventory/InventoryStore.kt index 4c8f32a..daf6813 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/inventory/InventoryStore.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/services/inventory/InventoryStore.kt @@ -1,4 +1,4 @@ -package com.pixelized.server.lwa.model.inventory +package com.pixelized.server.lwa.services.inventory import com.pixelized.server.lwa.server.exception.BusinessException import com.pixelized.server.lwa.server.exception.FileReadException diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/item/ItemService.kt b/server/src/main/kotlin/com/pixelized/server/lwa/services/item/ItemService.kt similarity index 96% rename from server/src/main/kotlin/com/pixelized/server/lwa/model/item/ItemService.kt rename to server/src/main/kotlin/com/pixelized/server/lwa/services/item/ItemService.kt index ed03b38..9e07dac 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/item/ItemService.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/services/item/ItemService.kt @@ -1,4 +1,4 @@ -package com.pixelized.server.lwa.model.item +package com.pixelized.server.lwa.services.item import com.pixelized.shared.lwa.model.item.ItemJson import com.pixelized.shared.lwa.model.item.factory.ItemJsonFactory diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/item/ItemStore.kt b/server/src/main/kotlin/com/pixelized/server/lwa/services/item/ItemStore.kt similarity index 99% rename from server/src/main/kotlin/com/pixelized/server/lwa/model/item/ItemStore.kt rename to server/src/main/kotlin/com/pixelized/server/lwa/services/item/ItemStore.kt index 135977a..59e9dd6 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/item/ItemStore.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/services/item/ItemStore.kt @@ -1,4 +1,4 @@ -package com.pixelized.server.lwa.model.item +package com.pixelized.server.lwa.services.item import com.pixelized.server.lwa.server.exception.BusinessException import com.pixelized.server.lwa.server.exception.FileReadException diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/services/map/MapService.kt b/server/src/main/kotlin/com/pixelized/server/lwa/services/map/MapService.kt new file mode 100644 index 0000000..c7002d2 --- /dev/null +++ b/server/src/main/kotlin/com/pixelized/server/lwa/services/map/MapService.kt @@ -0,0 +1,32 @@ +package com.pixelized.server.lwa.services.map + +import com.pixelized.server.lwa.server.exception.BusinessException +import com.pixelized.shared.lwa.model.map.MapJson + +class MapService( + private val mapStore: MapStore, +) { + fun maps(): List { + return mapStore.maps().value.values.toList() + } + + @Throws + fun map(id: String): MapJson { + return mapStore.maps().value[id] + ?: throw BusinessException("Map with id:$id is not found") + } + + @Throws + suspend fun save( + json: MapJson, + ) { + mapStore.save( + map = json, + ) + } + + @Throws + fun delete(mapId: String) { + mapStore.delete(id = mapId) + } +} diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/services/map/MapStore.kt b/server/src/main/kotlin/com/pixelized/server/lwa/services/map/MapStore.kt new file mode 100644 index 0000000..fac94cd --- /dev/null +++ b/server/src/main/kotlin/com/pixelized/server/lwa/services/map/MapStore.kt @@ -0,0 +1,126 @@ +package com.pixelized.server.lwa.services.map + +import com.pixelized.server.lwa.server.exception.BusinessException +import com.pixelized.server.lwa.server.exception.FileReadException +import com.pixelized.server.lwa.server.exception.FileWriteException +import com.pixelized.server.lwa.server.exception.JsonCodingException +import com.pixelized.server.lwa.server.exception.JsonConversionException +import com.pixelized.shared.lwa.model.map.MapJson +import com.pixelized.shared.lwa.utils.PathProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json +import java.io.File +import java.text.Collator + +class MapStore( + private val pathProvider: PathProvider, + private val jsonSerializer: Json, + scope: CoroutineScope, +) { + private val directory = File(pathProvider.mapPath()).also { it.mkdirs() } + private val mapsFlow = MutableStateFlow>(emptyMap()) + + init { + // make the file path. + File(pathProvider.mapPath()).mkdirs() + // load the initial data + scope.launch { + updateMapFlow() + } + } + + fun maps(): StateFlow> = mapsFlow + + suspend fun updateMapFlow() { + mapsFlow.value = try { + load().associateBy { it.id } + } catch (exception: Exception) { + println(exception.message) // TODO proper exception handling + emptyMap() + } + } + + private suspend fun load( + directory: File = this.directory, + ): List { + return withContext(Dispatchers.IO) { + directory + .listFiles() + ?.mapNotNull { file -> + val json = try { + file.readText(charset = Charsets.UTF_8) + } catch (exception: Exception) { + throw FileReadException(root = exception) + } + // Guard, if the json is blank no character have been save, ignore this file. + if (json.isBlank()) { + return@mapNotNull null + } + // decode the file + try { + jsonSerializer.decodeFromString(json) + } catch (exception: Exception) { + throw JsonCodingException(root = exception) + } + } + ?: emptyList() + } + } + + @Throws(JsonConversionException::class, FileWriteException::class) + suspend fun save(map: MapJson) { + // convert the data to json format + val json = try { + this.jsonSerializer.encodeToString(map) + } catch (exception: Exception) { + throw JsonConversionException(root = exception) + } + // write the file + try { + val file = mapFile(id = map.id) + file.writeText( + text = json, + charset = Charsets.UTF_8, + ) + } catch (exception: Exception) { + throw FileWriteException(root = exception) + } + // Update the dataflow. + mapsFlow.update { maps -> + maps.toMutableMap().also { + it[map.id] = map + } + } + } + + @Throws(BusinessException::class) + fun delete(id: String) { + val file = mapFile(id = id) + // Guard case on the file existence. + if (file.exists().not()) { + throw BusinessException( + message = "Alteration doesn't not exist, deletion is impossible.", + ) + } + // Guard case on the file deletion + if (file.delete().not()) { + throw BusinessException( + message = "Alteration file have not been deleted for unknown reason.", + ) + } + // Update the data model with the deleted alteration. + mapsFlow.update { maps -> + maps.toMutableMap().also { + it.remove(key = id) + } + } + } + + private fun mapFile(id: String) = File("${pathProvider.mapPath()}$id.json") +} diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/tag/TagService.kt b/server/src/main/kotlin/com/pixelized/server/lwa/services/tag/TagService.kt similarity index 90% rename from server/src/main/kotlin/com/pixelized/server/lwa/model/tag/TagService.kt rename to server/src/main/kotlin/com/pixelized/server/lwa/services/tag/TagService.kt index 6b2ecbf..f0ce351 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/tag/TagService.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/services/tag/TagService.kt @@ -1,4 +1,4 @@ -package com.pixelized.server.lwa.model.tag +package com.pixelized.server.lwa.services.tag import com.pixelized.shared.lwa.model.tag.TagJson diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/tag/TagStore.kt b/server/src/main/kotlin/com/pixelized/server/lwa/services/tag/TagStore.kt similarity index 97% rename from server/src/main/kotlin/com/pixelized/server/lwa/model/tag/TagStore.kt rename to server/src/main/kotlin/com/pixelized/server/lwa/services/tag/TagStore.kt index 4fd02b7..c93f552 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/tag/TagStore.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/services/tag/TagStore.kt @@ -1,4 +1,4 @@ -package com.pixelized.server.lwa.model.tag +package com.pixelized.server.lwa.services.tag import com.pixelized.server.lwa.server.exception.FileReadException import com.pixelized.server.lwa.server.exception.FileWriteException @@ -6,8 +6,6 @@ import com.pixelized.server.lwa.server.exception.JsonConversionException import com.pixelized.shared.lwa.model.tag.TagJson import com.pixelized.shared.lwa.utils.PathProvider import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/utils/extentions/ParametersExt.kt b/server/src/main/kotlin/com/pixelized/server/lwa/utils/extentions/ParametersExt.kt index 0475558..2abc6ef 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/utils/extentions/ParametersExt.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/utils/extentions/ParametersExt.kt @@ -43,6 +43,12 @@ val Parameters.itemId: String code = APIResponse.ErrorCode.ItemId, ) +val Parameters.mapId: String + get() = param( + name = "mapId", + code = APIResponse.ErrorCode.ItemId, + ) + val Parameters.count: Float get() = param( name = "count", diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/Module.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/Module.kt index c3a8b5f..742fcef 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/Module.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/Module.kt @@ -11,6 +11,8 @@ import com.pixelized.shared.lwa.model.inventory.factory.InventoryJsonFactory import com.pixelized.shared.lwa.model.inventory.factory.InventoryJsonFactoryV1 import com.pixelized.shared.lwa.model.item.factory.ItemJsonFactory import com.pixelized.shared.lwa.model.item.factory.ItemJsonFactoryV1 +import com.pixelized.shared.lwa.model.map.factory.MapJsonFactory +import com.pixelized.shared.lwa.model.map.factory.MapJsonFactoryV1 import com.pixelized.shared.lwa.model.tag.TagJsonFactory import com.pixelized.shared.lwa.parser.dice.DiceParser import com.pixelized.shared.lwa.parser.expression.ExpressionParser @@ -58,6 +60,8 @@ val factoryDependencies factoryOf(::ItemJsonFactoryV1) factoryOf(::InventoryJsonFactory) factoryOf(::InventoryJsonFactoryV1) + factoryOf(::MapJsonFactory) + factoryOf(::MapJsonFactoryV1) } val parserDependencies 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 2633b1c..4e65980 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 @@ -1,13 +1,25 @@ package com.pixelized.shared.lwa.model.campaign +import com.pixelized.shared.lwa.model.map.Map + data class Campaign( val characters: Set, val npcs: Set, + val map: MiniMap?, val scene: Scene, val options: Options, ) { val instances = characters + npcs + data class MiniMap( + val id: String, + val camera: Map.Camera?, + ) { + companion object { + fun empty(): MiniMap? = null + } + } + data class Scene( val name: String, ) { @@ -34,6 +46,7 @@ data class Campaign( fun empty() = Campaign( characters = emptySet(), npcs = emptySet(), + map = null, scene = Scene.empty(), options = Options.empty(), ) diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/CampaignJson.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/CampaignJson.kt index 6fc8fe9..a92d7df 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/CampaignJson.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/CampaignJson.kt @@ -12,6 +12,9 @@ sealed interface CampaignJson { sealed interface CharacteristicJson } + @Serializable + sealed interface MiniMap + @Serializable sealed interface SceneJson diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/CampaignJsonV2.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/CampaignJsonV2.kt index 26d0b9c..fe3c189 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/CampaignJsonV2.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/CampaignJsonV2.kt @@ -6,10 +6,24 @@ import kotlinx.serialization.Serializable data class CampaignJsonV2( val characters: Set, val npcs: Set, + val map: MiniMapJson?, val scene: SceneJsonV2?, val options: OptionsJsonV2?, ) : CampaignJson { + @Serializable + data class MiniMapJson( + val id: String, + val camera: CameraJson?, + ): CampaignJson.MiniMap { + @Serializable + data class CameraJson( + val zoom: Float, + val offsetX: Int, + val offsetY: Int, + ) + } + @Serializable data class SceneJsonV2( val name: String, diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/factory/CampaignJsonFactory.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/factory/CampaignJsonFactory.kt index 1a4314d..da2b49e 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/factory/CampaignJsonFactory.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/factory/CampaignJsonFactory.kt @@ -27,6 +27,14 @@ class CampaignJsonFactory( } } + fun convertFromJson( + json: CampaignJson.MiniMap + ): Campaign.MiniMap { + return when (json) { + is CampaignJsonV2.MiniMapJson -> v2.convertFromJson(mapJson = json) + } + } + // Json conversion. fun createScene( @@ -43,6 +51,18 @@ class CampaignJsonFactory( return CampaignJsonV2( characters = campaign.characters, npcs = campaign.npcs, + map = campaign.map?.let { campaign -> + CampaignJsonV2.MiniMapJson( + id = campaign.id, + camera = campaign.camera?.let { camera -> + CampaignJsonV2.MiniMapJson.CameraJson( + zoom = camera.zoom, + offsetX = camera.offsetX, + offsetY = camera.offsetY, + ) + } + ) + }, scene = CampaignJsonV2.SceneJsonV2( name = campaign.scene.name, ), diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/factory/CampaignJsonV1Factory.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/factory/CampaignJsonV1Factory.kt index 3053833..3a42393 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/factory/CampaignJsonV1Factory.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/factory/CampaignJsonV1Factory.kt @@ -14,6 +14,7 @@ class CampaignJsonV1Factory { return Campaign( characters = emptySet(), npcs = emptySet(), + map = null, scene = campaignJson.scene ?.let { convertFromJson(it) } ?: Campaign.Scene.empty(), diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/factory/CampaignJsonV2Factory.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/factory/CampaignJsonV2Factory.kt index 40598c2..9275ca5 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/factory/CampaignJsonV2Factory.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/campaign/factory/CampaignJsonV2Factory.kt @@ -2,6 +2,7 @@ package com.pixelized.shared.lwa.model.campaign.factory import com.pixelized.shared.lwa.model.campaign.Campaign import com.pixelized.shared.lwa.model.campaign.CampaignJsonV2 +import com.pixelized.shared.lwa.model.map.Map class CampaignJsonV2Factory { @@ -11,15 +12,33 @@ class CampaignJsonV2Factory { return Campaign( characters = campaignJson.characters, npcs = campaignJson.npcs, + map = campaignJson.map + ?.let { convertFromJson(mapJson = it) } + ?: Campaign.MiniMap.empty(), scene = campaignJson.scene - ?.let { convertFromJson(it) } + ?.let { convertFromJson(sceneJson = it) } ?: Campaign.Scene.empty(), options = campaignJson.options - ?.let { convertFromJson(it) } + ?.let { convertFromJson(optionsJson = it) } ?: Campaign.Options.empty() ) } + fun convertFromJson( + mapJson: CampaignJsonV2.MiniMapJson, + ): Campaign.MiniMap { + return Campaign.MiniMap( + id = mapJson.id, + camera = mapJson.camera?.let { + Map.Camera( + zoom = it.zoom, + offsetX = it.offsetX, + offsetY = it.offsetY, + ) + } + ) + } + fun convertFromJson( sceneJson: CampaignJsonV2.SceneJsonV2, ): Campaign.Scene { diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/map/Map.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/map/Map.kt new file mode 100644 index 0000000..7ea8a98 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/map/Map.kt @@ -0,0 +1,26 @@ +package com.pixelized.shared.lwa.model.map + +data class Map( + val id: String, + val name : String, + val camera: Camera, + val size: Size, + val resources: List, +) { + data class Camera( + val zoom: Float, + val offsetX: Int, + val offsetY: Int, + ) + + data class Size( + val width: Int, + val height: Int, + ) + + data class Resource( + val id: String, + val name: String, + val uri: String, + ) +} diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/map/MapJson.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/map/MapJson.kt new file mode 100644 index 0000000..b68a8c4 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/map/MapJson.kt @@ -0,0 +1,8 @@ +package com.pixelized.shared.lwa.model.map + +import kotlinx.serialization.Serializable + +@Serializable +sealed interface MapJson { + val id: String +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/map/MapJsonV1.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/map/MapJsonV1.kt new file mode 100644 index 0000000..8748ddf --- /dev/null +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/map/MapJsonV1.kt @@ -0,0 +1,32 @@ +package com.pixelized.shared.lwa.model.map + +import kotlinx.serialization.Serializable + +@Serializable +data class MapJsonV1( + override val id: String, + val name: String, + val camera: Camera, + val size: Size, + val resources: List, +) : MapJson { + @Serializable + data class Camera( + val zoom: Float, + val offsetX: Int, + val offsetY: Int, + ) + + @Serializable + data class Size( + val width: Int, + val height: Int, + ) + + @Serializable + data class Resource( + val id: String, + val name: String, + val uri: String, + ) +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/map/factory/MapJsonFactory.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/map/factory/MapJsonFactory.kt new file mode 100644 index 0000000..a920c19 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/map/factory/MapJsonFactory.kt @@ -0,0 +1,43 @@ +package com.pixelized.shared.lwa.model.map.factory + +import com.pixelized.shared.lwa.model.map.Map +import com.pixelized.shared.lwa.model.map.MapJson +import com.pixelized.shared.lwa.model.map.MapJsonV1 + +class MapJsonFactory( + private val v1: MapJsonFactoryV1, +) { + + fun convertFromJson( + json: MapJson, + ): Map { + return when (json) { + is MapJsonV1 -> v1.convertFromJson(json = json) + } + } + + fun convertToJson( + item: Map, + ): MapJson { + return MapJsonV1( + id = item.id, + name = item.name, + camera = MapJsonV1.Camera( + zoom = item.camera.zoom, + offsetX = item.camera.offsetX, + offsetY = item.camera.offsetY, + ), + size = MapJsonV1.Size( + width = item.size.width, + height = item.size.height, + ), + resources = item.resources.map { + MapJsonV1.Resource( + id = it.id, + name = it.name, + uri = it.uri, + ) + }, + ) + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/map/factory/MapJsonFactoryV1.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/map/factory/MapJsonFactoryV1.kt new file mode 100644 index 0000000..96dfc5f --- /dev/null +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/map/factory/MapJsonFactoryV1.kt @@ -0,0 +1,31 @@ +package com.pixelized.shared.lwa.model.map.factory + +import com.pixelized.shared.lwa.model.map.Map +import com.pixelized.shared.lwa.model.map.MapJsonV1 + +class MapJsonFactoryV1 { + fun convertFromJson( + json: MapJsonV1, + ): Map { + return Map( + id = json.id, + name = json.name, + camera = Map.Camera( + zoom = json.camera.zoom, + offsetX = json.camera.offsetX, + offsetY = json.camera.offsetY, + ), + size = Map.Size( + width = json.size.width, + height = json.size.height, + ), + resources = json.resources.map { + Map.Resource( + id = it.id, + name = it.name, + uri = it.uri, + ) + }, + ) + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/ApiSynchronisation.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/ApiSynchronisation.kt index ebed79e..05382ef 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/ApiSynchronisation.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/ApiSynchronisation.kt @@ -74,4 +74,19 @@ sealed interface ApiSynchronisation : SocketMessage { override val timestamp: Long, override val characterSheetId: String, ) : InventoryApiSynchronisation + + @Serializable + sealed interface MapApiSynchronisation : ApiSynchronisation + + @Serializable + data class MapUpdate( + override val timestamp: Long, + val id: String, + ) : MapApiSynchronisation + + @Serializable + data class MapDelete( + override val timestamp: Long, + val id: String, + ) : MapApiSynchronisation } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/CampaignEvent.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/CampaignEvent.kt index 8a9e972..3c2ba5b 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/CampaignEvent.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/CampaignEvent.kt @@ -1,5 +1,6 @@ package com.pixelized.shared.lwa.protocol.websocket +import com.pixelized.shared.lwa.model.campaign.CampaignJson import kotlinx.serialization.Serializable @Serializable @@ -7,7 +8,7 @@ sealed interface CampaignEvent : SocketMessage { // TODO Will probably be moved when the scene become more complex, (should already actually) @Serializable - data class UpdateScene( + data class SceneUpdated( override val timestamp: Long, val name: String, ) : CampaignEvent @@ -35,4 +36,15 @@ sealed interface CampaignEvent : SocketMessage { override val timestamp: Long, override val characterSheetId: String, ) : CampaignEvent, CharacterSheetIdMessage + + @Serializable + data class MapUpdated( + override val timestamp: Long, + val map: CampaignJson.MiniMap, + ): CampaignEvent + + @Serializable + data class MapDeleted( + override val timestamp: Long, + ): CampaignEvent } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/utils/PathProvider.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/utils/PathProvider.kt index 943be93..a8ab06a 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/utils/PathProvider.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/utils/PathProvider.kt @@ -83,4 +83,14 @@ class PathProvider( OperatingSystem.Macintosh -> "${storePath(os = os, app = app)}inventory/" } } + + fun mapPath( + os: OperatingSystem = this.operatingSystem, + app: String = this.appName, + ): String { + return when (os) { + OperatingSystem.Windows -> "${storePath(os = os, app = app)}maps\\" + OperatingSystem.Macintosh -> "${storePath(os = os, app = app)}maps/" + } + } }