Add map management to the server (REST+WS)
This commit is contained in:
parent
3485b8a9fd
commit
03dbd7aad6
62 changed files with 1226 additions and 144 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<List<TagJson>>
|
||||
|
||||
suspend fun getMaps(): APIResponse<List<MapJson>>
|
||||
|
||||
suspend fun getMap(
|
||||
mapId: String,
|
||||
): APIResponse<MapJson>
|
||||
|
||||
suspend fun putMap(
|
||||
mapJson: MapJson,
|
||||
create: Boolean,
|
||||
): APIResponse<Unit>
|
||||
|
||||
companion object {
|
||||
fun error(error: APIResponse<*>): Nothing = throw LwaNetworkException(error)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<List<TagJson>> = client
|
||||
.get("$root/tag/item")
|
||||
.body()
|
||||
|
||||
@Throws
|
||||
override suspend fun getMaps(): APIResponse<List<MapJson>> = client
|
||||
.get("$root/map/all")
|
||||
.body()
|
||||
|
||||
@Throws
|
||||
override suspend fun getMap(mapId: String): APIResponse<MapJson> = client
|
||||
.get("$root/map/detail?mapId=$mapId")
|
||||
.body()
|
||||
|
||||
@Throws
|
||||
override suspend fun putMap(
|
||||
mapJson: MapJson,
|
||||
create: Boolean,
|
||||
): APIResponse<Unit> = client
|
||||
.put("$root/map/update") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(mapJson)
|
||||
}
|
||||
.body<APIResponse<Unit>>()
|
||||
}
|
||||
|
|
@ -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,)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Map<String, MiniMap>> = mapStore.mapsFlow()
|
||||
|
||||
fun maps() = mapStore.mapsFlow().value
|
||||
|
||||
fun currentMap(): Flow<MiniMap?> = 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Map<String, MiniMap>>(emptyMap())
|
||||
|
||||
init {
|
||||
val scope = CoroutineScope(Dispatchers.IO + Job())
|
||||
// data update through WebSocket.
|
||||
scope.launch {
|
||||
network.data.collect(::handleMessage)
|
||||
}
|
||||
}
|
||||
|
||||
fun mapsFlow(): StateFlow<Map<String, MiniMap>> = 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<String, MiniMap> {
|
||||
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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17,15 +17,10 @@ import com.pixelized.desktop.lwa.ui.composable.scene.drawables.SceneLayer
|
|||
|
||||
@Stable
|
||||
data class Scene(
|
||||
val size: IntSize,
|
||||
val layers: List<SceneLayer>,
|
||||
val elements: List<SceneElement>,
|
||||
) {
|
||||
val size: IntSize = IntSize(
|
||||
width = layers.maxOf { it.size.width },
|
||||
height = layers.maxOf { it.size.height },
|
||||
)
|
||||
}
|
||||
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun Scene(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<Boolean>,
|
||||
enableLayersToggle: State<Boolean>,
|
||||
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,
|
||||
|
|
@ -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,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Boolean> 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> = _camera
|
||||
|
||||
private val _scene = MutableStateFlow(defaultScene())
|
||||
val scene: StateFlow<Scene> = _scene
|
||||
|
||||
val enableLayersToggle: StateFlow<Boolean> = 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()
|
||||
)
|
||||
}
|
||||
|
|
@ -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<String>,
|
||||
status: State<NetworkRepository.Status>,
|
||||
isAdmin: State<Boolean>,
|
||||
displayMapButton: State<Boolean>,
|
||||
menusState: State<CampaignMenuStateUio>,
|
||||
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,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue