diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_foggy_24dp.xml b/composeApp/src/commonMain/composeResources/drawable/ic_foggy_24dp.xml new file mode 100644 index 0000000..1c6e5d6 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/ic_foggy_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_foggy_filled_24dp.xml b/composeApp/src/commonMain/composeResources/drawable/ic_foggy_filled_24dp.xml new file mode 100644 index 0000000..ba07634 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/ic_foggy_filled_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/Camera.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/Camera.kt index c9eae2f..27d3733 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/Camera.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/Camera.kt @@ -2,15 +2,32 @@ package com.pixelized.desktop.lwa.ui.composable.scene import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.VectorConverter +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastRoundToInt +import com.pixelized.desktop.lwa.ui.composable.scene.drawables.SceneLayer +import com.pixelized.desktop.lwa.ui.theme.lwa +import com.pixelized.desktop.lwa.ui.theme.typography.LwaTypography @Stable class Camera( @@ -98,4 +115,63 @@ class Camera( else -> _zoom.animateTo(targetValue = initialZoom) } } +} + +@Composable +fun SceneCameraDebug( + modifier: Modifier = Modifier, + camera: Camera, + isOpen: Boolean = true, + style: LwaTypography.Debug = MaterialTheme.lwa.typography.debug, + padding: Dp = MaterialTheme.lwa.dimen.debug.offset, +) { + val isOpen = remember { mutableStateOf(isOpen) } + + Column( + modifier = Modifier + .clickable { isOpen.value = isOpen.value.not() } + .widthIn(min = MaterialTheme.lwa.dimen.debug.panelWidth) + .then(other = modifier), + verticalArrangement = Arrangement.spacedBy(space = 2.dp), + ) { + Text( + style = MaterialTheme.lwa.typography.debug.title, + color = MaterialTheme.lwa.colorScheme.base.primary, + text = "Camera", + ) + if (isOpen.value) { + Text( + modifier = Modifier.padding(start = padding), + style = MaterialTheme.lwa.typography.debug.propertyValue, + text = buildAnnotatedString { + withStyle(style.propertyId) { append("offset: ") } + append(camera.offset.toString()) + }, + ) + Text( + modifier = Modifier.padding(start = padding), + style = MaterialTheme.lwa.typography.debug.propertyValue, + text = buildAnnotatedString { + withStyle(style.propertyId) { append("size: ") } + append(camera.cameraSize.toString()) + }, + ) + Text( + modifier = Modifier.padding(start = padding), + style = MaterialTheme.lwa.typography.debug.propertyValue, + text = buildAnnotatedString { + withStyle(style.propertyId) { append("zoom: ") } + append(camera.zoom.toString()) + }, + ) + Text( + modifier = Modifier.padding(start = padding), + style = MaterialTheme.lwa.typography.debug.propertyValue, + text = buildAnnotatedString { + withStyle(style.propertyId) { append("size: ") } + append(camera.cameraSizeZoomed.toString()) + }, + ) + } + } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/Cursor.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/Cursor.kt new file mode 100644 index 0000000..a3ee09b --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/Cursor.kt @@ -0,0 +1,75 @@ +package com.pixelized.desktop.lwa.ui.composable.scene + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import com.pixelized.desktop.lwa.ui.theme.lwa +import com.pixelized.desktop.lwa.ui.theme.typography.LwaTypography + +@Stable +class Cursor( + initial: IntOffset = IntOffset.Zero, +) { + private val _offset = mutableStateOf(initial) + val offset by _offset + + fun change( + position: Offset, + ) { + _offset.value = IntOffset( + x = position.x.toInt(), + y = position.y.toInt(), + ) + } +} + +@Composable +fun SceneCursorDebug( + modifier: Modifier = Modifier, + cursor: Cursor, + isOpen: Boolean = true, + style: LwaTypography.Debug = MaterialTheme.lwa.typography.debug, + padding: Dp = MaterialTheme.lwa.dimen.debug.offset, +) { + val isOpen = remember { mutableStateOf(isOpen) } + + Column( + modifier = Modifier + .clickable { isOpen.value = isOpen.value.not() } + .widthIn(min = MaterialTheme.lwa.dimen.debug.panelWidth) + .then(other = modifier), + verticalArrangement = Arrangement.spacedBy(space = 2.dp), + ) { + Text( + style = MaterialTheme.lwa.typography.debug.title, + color = MaterialTheme.lwa.colorScheme.base.primary, + text = "Cursor", + ) + if (isOpen.value) { + Text( + modifier = Modifier.padding(start = padding), + style = MaterialTheme.lwa.typography.debug.propertyValue, + text = buildAnnotatedString { + withStyle(style.propertyId) { append("offset: ") } + append(cursor.offset.toString()) + }, + ) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/DahomeMap.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/DahomeMap.kt new file mode 100644 index 0000000..349665e --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/DahomeMap.kt @@ -0,0 +1,177 @@ +package com.pixelized.desktop.lwa.ui.composable.scene + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +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 com.pixelized.desktop.lwa.ui.screen.campaign.LocalCampaignLayoutScope +import kotlinx.coroutines.launch +import lwacharactersheet.composeapp.generated.resources.Res +import lwacharactersheet.composeapp.generated.resources.ic_center_focus_weak_24dp +import lwacharactersheet.composeapp.generated.resources.ic_foggy_24dp +import lwacharactersheet.composeapp.generated.resources.ic_foggy_filled_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.icon_d100 +import lwacharactersheet.composeapp.generated.resources.image_dahome_maps +import lwacharactersheet.composeapp.generated.resources.image_dahome_regions +import org.jetbrains.compose.resources.painterResource + +@Composable +fun MapScene( + modifier: Modifier = Modifier, +) { + val campaign = LocalCampaignLayoutScope.current + val scope = rememberCoroutineScope() + val fogOfWarEdit = remember { mutableStateOf(true) } + + val map = rememberLayoutFromResource( + name = "Dahomé", + resource = Res.drawable.image_dahome_maps, + ) + val mapRegionOverlay = rememberLayoutFromResource( + name = "Région", + resource = Res.drawable.image_dahome_regions, + ) + val element1 = rememberElementFromResource( + name = "Group", + resource = Res.drawable.icon_d100, + offset = IntOffset( + x = 2128, + y = 1875, + ), + ) + val element2 = rememberElementFromResource( + name = "End", + resource = Res.drawable.icon_d100, + offset = IntOffset( + x = map.size.width, + y = map.size.height, + ) + ) + val element3 = rememberElementFromResource( + name = "Start", + resource = Res.drawable.icon_d100, + offset = IntOffset( + x = 0, + y = 0, + ) + ) + val scene = remember(map, mapRegionOverlay, element1, element2) { + Scene( + camera = Camera( + initialZoom = 1f, + initialOffset = IntOffset(x = -150, y = -120), + ), + fogOfWar = FogOfWar(), + layers = listOf( + map, + mapRegionOverlay, + ), + elements = listOf( + element1, + element2, + element3, + ), + ) + } + Scene( + modifier = modifier.onFogOfWarControl( + scope = scope, + enable = fogOfWarEdit.value, + fogOfWar = scene.fogOfWar, + camera = scene.camera, + ), + scene = scene, + ) { + Row( + modifier = Modifier + .align(alignment = Alignment.BottomStart) + .padding( + start = campaign.leftPanel.value.width, + bottom = campaign.chatOverlay.value.height, + ) + ) { + IconButton( + onClick = { + scope.launch { + scene.camera.handleZoom( + zoomIn = true, + power = 0.3f, + ) + } + } + ) { + Icon( + painter = painterResource(Res.drawable.ic_zoom_in_map_24dp), + contentDescription = null + ) + } + IconButton( + onClick = { + scope.launch { + scene.camera.handleZoom( + zoomIn = false, + power = 0.3f, + ) + } + } + ) { + Icon( + painter = painterResource(Res.drawable.ic_zoom_out_map_24dp), + contentDescription = null + ) + } + IconButton( + onClick = { + scope.launch { + scene.camera.resetPosition() + } + scope.launch { + scene.camera.resetZoom() + } + } + ) { + Icon( + painter = painterResource(Res.drawable.ic_center_focus_weak_24dp), + contentDescription = null + ) + } + IconButton( + onClick = { + scope.launch { + scene.layers.getOrNull(1)?.let { + it.alpha(alpha = if (it.alpha == 0f) 1f else 0f) + } + } + } + ) { + Icon( + painter = painterResource(Res.drawable.ic_visibility_24dp), + contentDescription = null + ) + } + IconButton( + onClick = { + fogOfWarEdit.value = fogOfWarEdit.value.not() + } + ) { + Icon( + painter = when (fogOfWarEdit.value) { + true -> painterResource(Res.drawable.ic_foggy_filled_24dp) + else -> painterResource(Res.drawable.ic_foggy_24dp) + }, + contentDescription = null + ) + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/FogOfWar.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/FogOfWar.kt index 31e9e09..b8bec27 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/FogOfWar.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/FogOfWar.kt @@ -1,9 +1,78 @@ package com.pixelized.desktop.lwa.ui.composable.scene import androidx.compose.runtime.Stable +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.input.pointer.PointerEvent +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.isPrimaryPressed +import androidx.compose.ui.input.pointer.onPointerEvent +import com.pixelized.desktop.lwa.ui.composable.scene.utils.global +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch @Stable -data class FogOfWar( +class FogOfWar( val color: Color = Color.Black.copy(alpha = 0.5f), -) \ No newline at end of file +) { + val path = Path() + + fun moveTo(position: Offset) { + path.moveTo(x = position.x, y = position.y) + } + + fun lineTo(position: Offset) { + path.lineTo(x = position.x, y = position.y) + } + + companion object { + val NONE = FogOfWar(color = Color.Transparent) + } +} + +@OptIn(ExperimentalComposeUiApi::class) +fun Modifier.onFogOfWarControl( + scope: CoroutineScope, + enable: Boolean, + camera: Camera, + fogOfWar: FogOfWar, +) = if (!enable) { + this +} else { + var lastEventTime = System.currentTimeMillis() + var previousEvent: PointerEvent? = null + + this + .onPointerEvent(PointerEventType.Release) { event: PointerEvent -> + scope.launch { + println("PointerEventType.Release") + lastEventTime = System.currentTimeMillis() + previousEvent = null + } + } + .onPointerEvent(PointerEventType.Move) { event: PointerEvent -> + scope.launch { + val pointer = event.changes.firstOrNull() + val time = pointer?.uptimeMillis ?: 0L + + if (time - lastEventTime > 10L && event.buttons.isPrimaryPressed) { + if (previousEvent?.buttons?.isPrimaryPressed == true) { + println("PointerEventType.LineTo") + pointer?.position + ?.global(camera = camera) + ?.let(fogOfWar::lineTo) + } else { + println("PointerEventType.MoveTo") + pointer?.position + ?.global(camera = camera) + ?.let(fogOfWar::moveTo) + } + lastEventTime = System.currentTimeMillis() + previousEvent = event + } + } + } +} \ 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 9e2ed21..21e6fb8 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 @@ -2,12 +2,9 @@ package com.pixelized.desktop.lwa.ui.composable.scene import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.padding -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Add +import androidx.compose.material.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.remember @@ -17,6 +14,15 @@ import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.CompositingStrategy +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.StrokeJoin +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.drawscope.scale +import androidx.compose.ui.graphics.drawscope.translate import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.PointerEvent import androidx.compose.ui.input.pointer.PointerEventType @@ -28,163 +34,106 @@ import androidx.compose.ui.input.pointer.onPointerEvent import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntSize -import androidx.compose.ui.unit.dp -import com.pixelized.desktop.lwa.ui.screen.campaign.LocalCampaignLayoutScope +import androidx.compose.ui.unit.toSize +import com.pixelized.desktop.lwa.ui.composable.scene.drawables.SceneElement +import com.pixelized.desktop.lwa.ui.composable.scene.drawables.SceneLayer +import com.pixelized.desktop.lwa.ui.composable.scene.utils.global +import com.pixelized.desktop.lwa.ui.composable.scene.utils.local +import com.pixelized.desktop.lwa.ui.theme.lwa import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import lwacharactersheet.composeapp.generated.resources.Res -import lwacharactersheet.composeapp.generated.resources.ic_center_focus_weak_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.ic_visibility_24dp -import lwacharactersheet.composeapp.generated.resources.icon_d100 -import lwacharactersheet.composeapp.generated.resources.image_dahome_maps -import lwacharactersheet.composeapp.generated.resources.image_dahome_regions import org.jetbrains.compose.resources.DrawableResource import org.jetbrains.compose.resources.imageResource -import org.jetbrains.compose.resources.painterResource +import java.util.UUID import kotlin.math.sign @Stable data class Scene( val camera: Camera, - val layouts: List, val fogOfWar: FogOfWar, + val layers: List, + val elements: List, ) { val size: IntSize = IntSize( - width = layouts.maxOf { it.size.width }, - height = layouts.maxOf { it.size.height }, + width = layers.maxOf { it.size.width }, + height = layers.maxOf { it.size.height }, ) } + @Composable fun Scene( - modifier: Modifier, + modifier: Modifier = Modifier, + scene: Scene, + content: @Composable BoxScope.() -> Unit, ) { - val campaign = LocalCampaignLayoutScope.current val scope = rememberCoroutineScope() - val scene = rememberScene( - camera = Camera( - initialZoom = 1f, - initialOffset = IntOffset(x = -150, y = -120), - ), - fogOfWar = FogOfWar(), - rememberLayoutFromResource( - resource = Res.drawable.image_dahome_maps, - ), - rememberLayoutFromResource( - resource = Res.drawable.image_dahome_regions, - ), - rememberLayoutFromResource( - resource = Res.drawable.icon_d100, - offset = IntOffset(x = 1740, y = 910), - ), - ) + val cursors = remember { + listOf( + Cursor() + ) + } + Box( modifier = modifier .graphicsLayer { clip = true } .onCameraControl(scope = scope, scene = scene) - .drawScene(scene = scene) - .fogOfWar(scene = scene) + .onCursorControl(camera = scene.camera, cursor = cursors.first()) + .drawLayers(camera = scene.camera, layers = scene.layers) + .drawElements(camera = scene.camera, elements = scene.elements) + .drawCursors(camera = scene.camera, cursors = cursors) + .drawFogOfWar(scene = scene), ) { - Column( - modifier = Modifier - .align(alignment = Alignment.BottomEnd) - .padding(end = campaign.rightPanel.value.width) - .padding(all = 8.dp) - ) { - IconButton( - onClick = { - scope.launch { - scene.camera.handleZoom( - zoomIn = true, - power = 0.3f, - ) - } - } - ) { - Icon( - painter = painterResource(Res.drawable.ic_zoom_in_map_24dp), - contentDescription = null - ) - } - IconButton( - onClick = { - scope.launch { - scene.camera.handleZoom( - zoomIn = false, - power = 0.3f, - ) - } - } - ) { - Icon( - painter = painterResource(Res.drawable.ic_zoom_out_map_24dp), - contentDescription = null - ) - } - IconButton( - onClick = { - scope.launch { - scene.camera.resetPosition() - } - scope.launch { - scene.camera.resetZoom() - } - } - ) { - Icon( - painter = painterResource(Res.drawable.ic_center_focus_weak_24dp), - contentDescription = null - ) - } - IconButton( - onClick = { - scope.launch { - scene.layouts.getOrNull(1)?.let { - it.alpha(alpha = if (it.alpha == 0f) 1f else 0f) - } - } - } - ) { - Icon( - painter = painterResource(Res.drawable.ic_visibility_24dp), - contentDescription = null - ) - } - } - } -} + content() -@Composable -@Stable -fun rememberLayoutFromResource( - resource: DrawableResource, - offset: IntOffset = IntOffset.Zero, -): Layout { - val texture = imageResource( - resource = resource, - ) - return remember(resource) { - Layout( - texture = texture, - offset = offset, + SceneDebugPanel( + modifier = Modifier + .align(alignment = Alignment.TopEnd) + .padding(paddingValues = MaterialTheme.lwa.dimen.paddingValues), + cursor = cursors.first(), + scene = scene, ) } } @Composable @Stable -fun rememberScene( - camera: Camera, - fogOfWar: FogOfWar, - vararg layouts: Layout, -): Scene { - return remember { - Scene( - camera = camera, - layouts = layouts.toList(), - fogOfWar = fogOfWar, +fun rememberLayoutFromResource( + name: String, + resource: DrawableResource, + offset: IntOffset = IntOffset.Zero, +): SceneLayer { + val texture = imageResource( + resource = resource, + ) + return remember(resource) { + SceneLayer( + id = UUID.randomUUID().toString(), + name = name, + texture = texture, + offset = offset, + alpha = 1f, + ) + } +} + +@Composable +@Stable +fun rememberElementFromResource( + name: String, + resource: DrawableResource, + offset: IntOffset = IntOffset.Zero, +): SceneElement { + val texture = imageResource( + resource = resource, + ) + return remember(resource) { + SceneElement( + id = UUID.randomUUID().toString(), + name = name, + texture = texture, + offset = offset, + alpha = 1f, ) } } @@ -233,30 +182,117 @@ fun Modifier.onCameraControl( } } -fun Modifier.drawScene( - scene: Scene, +@OptIn(ExperimentalComposeUiApi::class) +fun Modifier.onCursorControl( + camera: Camera, + cursor: Cursor, +): Modifier = this + .onPointerEvent(PointerEventType.Exit) { event: PointerEvent -> + cursor.change( + position = Offset.Unspecified, + ) + } + .onPointerEvent(PointerEventType.Move) { event: PointerEvent -> + cursor.change( + position = event.changes.first().position.global(camera = camera), + ) + } + +private fun Modifier.drawCursors( + camera: Camera, + cursors: List, +): Modifier = this + .drawWithCache { + onDrawBehind { + cursors.forEach { cursor -> + drawRect( + color = Color.Green, + topLeft = cursor.offset.local(camera = camera), + size = Size(10f, 10f), + style = Stroke(width = 2f), + ) + } + } + } + +@OptIn(ExperimentalComposeUiApi::class) +fun Modifier.drawLayers( + camera: Camera, + layers: List, ): Modifier = this.drawWithCache { onDrawBehind { - scene.layouts.forEach { layout -> + layers.forEach { layers -> drawImage( - image = layout.texture, - srcOffset = scene.camera.offset - layout.offset, - srcSize = scene.camera.cameraSizeZoomed, - dstSize = scene.camera.cameraSize, - alpha = layout.alpha, + image = layers.texture, + srcOffset = camera.offset - layers.offset, + srcSize = camera.cameraSizeZoomed, + dstSize = camera.cameraSize, + alpha = layers.alpha, ) } } } -fun Modifier.fogOfWar( - scene: Scene, +fun Modifier.drawElements( + camera: Camera, + elements: List, ): Modifier = this.drawWithCache { onDrawBehind { - drawRect(color = scene.fogOfWar.color) + elements.forEach { element -> + drawImage( + image = element.texture, + srcOffset = camera.offset - element.position, + srcSize = camera.cameraSizeZoomed, + dstSize = camera.cameraSize, + alpha = element.alpha, + ) + drawRect( + color = Color.Red, + topLeft = element.position.local(camera = camera), + size = (element.size).toSize() / camera.zoom, + style = Stroke(width = 2f), + ) + } } } +fun Modifier.drawFogOfWar( + scene: Scene, +): Modifier = this + .graphicsLayer( + compositingStrategy = CompositingStrategy.Offscreen + ) + .drawWithCache { + val stroke = Stroke( + width = 32f, + cap = StrokeCap.Round, + join = StrokeJoin.Round + ) + val fog = Color.Black.copy(alpha = 0.5f) + val color = Color.Transparent + onDrawBehind { + drawRect( + color = fog, + ) + scale( + scale = 1 / scene.camera.zoom, + pivot = Offset.Zero, + ) { + translate( + left = -scene.camera.offset.x.toFloat(), + top = -scene.camera.offset.y.toFloat(), + ) { + drawPath( + path = scene.fogOfWar.path, + style = stroke, + color = color, + blendMode = BlendMode.Clear, + ) + } + } + } + } + private data class CursorDelta( var lastDeltaTimestamp: Long = System.currentTimeMillis(), var previousPosition: Offset = Offset.Zero, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/SceneDebugPanel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/SceneDebugPanel.kt new file mode 100644 index 0000000..45c99b7 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/SceneDebugPanel.kt @@ -0,0 +1,125 @@ +package com.pixelized.desktop.lwa.ui.composable.scene + +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Card +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +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 com.pixelized.desktop.lwa.ui.composable.scene.drawables.SceneElementDebug +import com.pixelized.desktop.lwa.ui.composable.scene.drawables.SceneLayerDebug +import com.pixelized.desktop.lwa.ui.theme.lwa +import com.pixelized.desktop.lwa.ui.theme.typography.LwaTypography + +@Composable +fun SceneDebugPanel( + modifier: Modifier = Modifier, + paddingValues: PaddingValues = MaterialTheme.lwa.dimen.paddingValues, + cursor: Cursor, + scene: Scene, +) { + Card( + modifier = Modifier + .verticalScroll(state = rememberScrollState()) + .then(other = modifier), + backgroundColor = MaterialTheme.lwa.colorScheme.elevated.base4dp, + ) { + Column( + modifier = Modifier.padding(paddingValues = paddingValues), + verticalArrangement = Arrangement.spacedBy(space = 8.dp), + ) { + SceneDebug( + modifier = Modifier.animateContentSize(), + scene = scene, + ) + SceneCursorDebug( + modifier = Modifier.animateContentSize(), + cursor = cursor, + ) + SceneCameraDebug( + modifier = Modifier.animateContentSize(), + camera = scene.camera, + ) + Column { + Text( + style = MaterialTheme.lwa.typography.debug.title, + text = "Layers:(${scene.layers.size})" + ) + scene.layers.forEach { layer -> + SceneLayerDebug( + modifier = Modifier + .animateContentSize() + .padding(start = MaterialTheme.lwa.dimen.debug.offset), + isOpen = false, + layer = layer, + ) + } + } + Column { + Text( + style = MaterialTheme.lwa.typography.debug.title, + text = "Elements:(${scene.elements.size})" + ) + scene.elements.forEach { element -> + SceneElementDebug( + modifier = Modifier + .animateContentSize() + .padding(start = MaterialTheme.lwa.dimen.debug.offset), + isOpen = false, + element = element, + ) + } + } + } + } + +} + +@Composable +private fun SceneDebug( + modifier: Modifier = Modifier, + scene: Scene, + isOpen: Boolean = true, + style: LwaTypography.Debug = MaterialTheme.lwa.typography.debug, + padding: Dp = MaterialTheme.lwa.dimen.debug.offset, +) { + val isOpen = remember { mutableStateOf(isOpen) } + + Column( + modifier = Modifier + .clickable { isOpen.value = isOpen.value.not() } + .widthIn(min = MaterialTheme.lwa.dimen.debug.panelWidth) + .then(other = modifier), + verticalArrangement = Arrangement.spacedBy(space = 2.dp), + ) { + Text( + style = MaterialTheme.lwa.typography.debug.title, + color = MaterialTheme.lwa.colorScheme.base.primary, + text = "Scene", + ) + if (isOpen.value) { + Text( + modifier = Modifier.padding(start = padding), + style = MaterialTheme.lwa.typography.debug.propertyValue, + text = buildAnnotatedString { + withStyle(style.propertyId) { append("Size: ") } + append(scene.size.toString()) + }, + ) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/Layout.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/drawables/SceneDrawable.kt similarity index 73% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/Layout.kt rename to composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/drawables/SceneDrawable.kt index e211690..43c377d 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/Layout.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/drawables/SceneDrawable.kt @@ -1,22 +1,22 @@ -package com.pixelized.desktop.lwa.ui.composable.scene +package com.pixelized.desktop.lwa.ui.composable.scene.drawables import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.VectorConverter -import androidx.compose.runtime.Stable import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntSize -@Stable -class Layout( +open class SceneDrawable( + val id: String, + val name: String, val texture: ImageBitmap, - val offset: IntOffset = IntOffset.Zero, - val size: IntSize = IntSize(texture.width, texture.height), - private val initialAlpha: Float = 1f, + val offset: IntOffset, + val size: IntSize, + private val initialAlpha: Float, ) { private val _alpha = Animatable( initialValue = initialAlpha, - typeConverter = Float.VectorConverter, + typeConverter = Float.Companion.VectorConverter, ) val alpha get() = _alpha.value diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/drawables/SceneElement.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/drawables/SceneElement.kt new file mode 100644 index 0000000..fae7da7 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/drawables/SceneElement.kt @@ -0,0 +1,104 @@ +package com.pixelized.desktop.lwa.ui.composable.scene.drawables + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import com.pixelized.desktop.lwa.ui.theme.lwa +import com.pixelized.desktop.lwa.ui.theme.typography.LwaTypography + +@Stable +class SceneElement( + id: String, + name: String, + texture: ImageBitmap, + offset: IntOffset = IntOffset.Companion.Zero, + size: IntSize = IntSize(texture.width, texture.height), + alpha: Float = 1f, +) : SceneDrawable( + id = id, + name = name, + texture = texture, + offset = offset, + size = size, + initialAlpha = alpha, +) { + val position = IntOffset( + x = offset.x - size.width / 2, + y = offset.y - size.height / 2, + ) +} + +@Composable +fun SceneElementDebug( + modifier: Modifier = Modifier, + element: SceneElement, + isOpen: Boolean = true, + style: LwaTypography.Debug = MaterialTheme.lwa.typography.debug, + padding: Dp = MaterialTheme.lwa.dimen.debug.offset, +) { + val isOpen = remember { mutableStateOf(isOpen) } + + Column( + modifier = Modifier + .clickable { isOpen.value = isOpen.value.not() } + .widthIn(min = MaterialTheme.lwa.dimen.debug.panelWidth) + .then(other = modifier), + verticalArrangement = Arrangement.spacedBy(space = 2.dp), + ) { + Text( + style = MaterialTheme.lwa.typography.debug.title, + color = MaterialTheme.lwa.colorScheme.base.primary, + text = element.name, + ) + if (isOpen.value) { + Text( + modifier = Modifier.padding(start = padding), + style = MaterialTheme.lwa.typography.debug.propertyValue, + text = buildAnnotatedString { + withStyle(style.propertyId) { append("id: ") } + append(element.id) + }, + ) + Text( + modifier = Modifier.padding(start = padding), + style = MaterialTheme.lwa.typography.debug.propertyValue, + text = buildAnnotatedString { + withStyle(style.propertyId) { append("offset: ") } + append(element.offset.toString()) + }, + ) + Text( + modifier = Modifier.padding(start = padding), + style = MaterialTheme.lwa.typography.debug.propertyValue, + text = buildAnnotatedString { + withStyle(style.propertyId) { append("size: ") } + append(element.size.toString()) + }, + ) + Text( + modifier = Modifier.padding(start = padding), + style = MaterialTheme.lwa.typography.debug.propertyValue, + text = buildAnnotatedString { + withStyle(style.propertyId) { append("alpha: ") } + append(element.alpha.toString()) + }, + ) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/drawables/SceneLayer.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/drawables/SceneLayer.kt new file mode 100644 index 0000000..6705afe --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/drawables/SceneLayer.kt @@ -0,0 +1,99 @@ +package com.pixelized.desktop.lwa.ui.composable.scene.drawables + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import com.pixelized.desktop.lwa.ui.theme.lwa +import com.pixelized.desktop.lwa.ui.theme.typography.LwaTypography + +@Stable +class SceneLayer( + id : String, + name: String, + texture: ImageBitmap, + offset: IntOffset = IntOffset.Companion.Zero, + size: IntSize = IntSize(texture.width, texture.height), + alpha: Float = 1f, +) : SceneDrawable( + id = id, + name = name, + texture = texture, + offset = offset, + size = size, + initialAlpha = alpha, +) + +@Composable +fun SceneLayerDebug( + modifier: Modifier = Modifier, + layer: SceneLayer, + isOpen: Boolean = true, + style: LwaTypography.Debug = MaterialTheme.lwa.typography.debug, + padding: Dp = MaterialTheme.lwa.dimen.debug.offset, +) { + val isOpen = remember { mutableStateOf(isOpen) } + + Column( + modifier = Modifier + .clickable { isOpen.value = isOpen.value.not() } + .widthIn(min = MaterialTheme.lwa.dimen.debug.panelWidth) + .then(other = modifier), + verticalArrangement = Arrangement.spacedBy(space = 2.dp), + ) { + Text( + style = MaterialTheme.lwa.typography.debug.title, + color = MaterialTheme.lwa.colorScheme.base.primary, + text = layer.name, + ) + if (isOpen.value) { + Text( + modifier = Modifier.padding(start = padding), + style = MaterialTheme.lwa.typography.debug.propertyValue, + text = buildAnnotatedString { + withStyle(style.propertyId) { append("id: ") } + append(layer.id) + }, + ) + Text( + modifier = Modifier.padding(start = padding), + style = MaterialTheme.lwa.typography.debug.propertyValue, + text = buildAnnotatedString { + withStyle(style.propertyId) { append("offset: ") } + append(layer.offset.toString()) + }, + ) + Text( + modifier = Modifier.padding(start = padding), + style = MaterialTheme.lwa.typography.debug.propertyValue, + text = buildAnnotatedString { + withStyle(style.propertyId) { append("size: ") } + append(layer.size.toString()) + }, + ) + Text( + modifier = Modifier.padding(start = padding), + style = MaterialTheme.lwa.typography.debug.propertyValue, + text = buildAnnotatedString { + withStyle(style.propertyId) { append("alpha: ") } + append(layer.alpha.toString()) + }, + ) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/utils/Offset+Camera.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/utils/Offset+Camera.kt new file mode 100644 index 0000000..853a544 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/utils/Offset+Camera.kt @@ -0,0 +1,39 @@ +package com.pixelized.desktop.lwa.ui.composable.scene.utils + +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.unit.IntOffset +import com.pixelized.desktop.lwa.ui.composable.scene.Camera + +/** + * Convert local positon to global one. + * Global position are agnostic from camera, and therefor should be use to position stuff on the map. + * A common use case is to share players cursor. + */ +fun Offset.global( + camera: Camera, +): Offset = Offset( + x = this.x * camera.zoom + camera.offset.x, + y = this.y * camera.zoom + camera.offset.y, +) + +/** + * Convert global positon to local one. + * Local position take into account the camera and are use to display stuff on the Scene composable. + */ +fun Offset.local( + camera: Camera, +): Offset = Offset( + x = (this.x - camera.offset.x) / camera.zoom, + y = (this.y - camera.offset.y) / camera.zoom, +) + +/** + * Convert global positon to local one. + * Local position take into account the camera and are use to display stuff on the Scene composable. + */ +fun IntOffset.local( + camera: Camera, +): Offset = Offset( + x = (this.x.toFloat() - camera.offset.x) / camera.zoom, + y = (this.y.toFloat() - camera.offset.y) / camera.zoom, +) \ No newline at end of file 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 cfce078..9abb099 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,7 @@ 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.composable.scene.Scene +import com.pixelized.desktop.lwa.ui.composable.scene.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 @@ -101,7 +101,7 @@ fun CampaignScreen( }, main = { - Scene( + MapScene( modifier = Modifier.matchParentSize(), ) }, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/common/CharacterRibbonStats.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/common/CharacterRibbonStats.kt index 2646a66..2659e2d 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/common/CharacterRibbonStats.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/player/ribbon/common/CharacterRibbonStats.kt @@ -4,7 +4,6 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding @@ -15,8 +14,11 @@ import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import com.pixelized.desktop.lwa.ui.theme.lwa import lwacharactersheet.composeapp.generated.resources.Res @@ -37,6 +39,11 @@ fun CharacterRibbonStats( modifier: Modifier = Modifier, status: CharacterRibbonStatsUio?, ) { + val typography = MaterialTheme.lwa.typography + val valueSpanStyle = remember(typography) { typography.portrait.value.toSpanStyle() } + val separatorSpanStyle = remember(typography) { typography.portrait.separator.toSpanStyle() } + val maxSpanStyle = remember(typography) { typography.portrait.max.toSpanStyle() } + status?.let { status -> Column( modifier = Modifier @@ -58,18 +65,14 @@ fun CharacterRibbonStats( ) Text( modifier = Modifier.alignByBaseline(), - style = MaterialTheme.lwa.typography.portrait.value, - text = "${status.hp}", - ) - Text( - modifier = Modifier.alignByBaseline(), - style = MaterialTheme.lwa.typography.portrait.separator, - text = "/", - ) - Text( - modifier = Modifier.alignByBaseline(), - style = MaterialTheme.lwa.typography.portrait.max, - text = "${status.maxHp}", + style = typography.portrait.value, + text = remember(status.hp, status.maxHp) { + buildAnnotatedString { + withStyle(style = valueSpanStyle) { append("${status.hp}") } + withStyle(style = separatorSpanStyle) { append("/") } + withStyle(style = maxSpanStyle) { append("${status.maxHp}") } + } + }, ) } Row { @@ -84,19 +87,14 @@ fun CharacterRibbonStats( modifier = Modifier.width(width = 2.dp), ) Text( - modifier = Modifier.alignByBaseline(), - style = MaterialTheme.lwa.typography.portrait.value, - text = "${status.pp}", - ) - Text( - modifier = Modifier.alignByBaseline(), - style = MaterialTheme.lwa.typography.portrait.separator, - text = "/", - ) - Text( - modifier = Modifier.alignByBaseline(), - style = MaterialTheme.lwa.typography.portrait.max, - text = "${status.maxPp}", + style = typography.portrait.value, + text = remember(status.pp, status.maxPp) { + buildAnnotatedString { + withStyle(style = valueSpanStyle) { append("${status.pp}") } + withStyle(style = separatorSpanStyle) { append("/") } + withStyle(style = maxSpanStyle) { append("${status.maxPp}") } + } + }, ) } } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/dimen/LwaDimen.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/dimen/LwaDimen.kt index 6c7aaa1..f2006aa 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/dimen/LwaDimen.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/dimen/LwaDimen.kt @@ -15,6 +15,7 @@ data class LwaDimen( val layout: Layout, val portrait: Portrait, val sheet: Sheet, + val debug: Debug, ) { @Stable data class Layout( @@ -33,6 +34,12 @@ data class LwaDimen( val subCategory: Dp, val characteristic: DpSize, ) + + @Stable + data class Debug( + val panelWidth: Dp, + val offset: Dp, + ) } @Composable @@ -52,6 +59,10 @@ fun lwaDimen( detailWidth = 128.dp * 4, chatMaxWidth = 600.dp, ), + debug: LwaDimen.Debug = LwaDimen.Debug( + panelWidth = 200.dp, + offset = 8.dp, + ) ): LwaDimen { return remember { LwaDimen( @@ -60,6 +71,7 @@ fun lwaDimen( portrait = portrait, sheet = sheet, layout = layout, + debug = debug, ) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/typography/LwaTypography.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/typography/LwaTypography.kt index e9f7afc..114042b 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/typography/LwaTypography.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/typography/LwaTypography.kt @@ -10,6 +10,7 @@ import androidx.compose.ui.graphics.Shadow import androidx.compose.ui.graphics.StrokeJoin import androidx.compose.ui.graphics.drawscope.Fill import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontStyle @@ -30,6 +31,7 @@ data class LwaTypography( val characterSheet: CharacterSheet, val inventory: Inventory, val freeDiceThrow: Dice, + val debug: Debug, ) { @Stable data class Chat( @@ -73,6 +75,13 @@ data class LwaTypography( val dice: TextStyle, val result: TextStyle, ) + + @Stable + data class Debug( + val title: TextStyle, + val propertyId: SpanStyle, + val propertyValue: TextStyle, + ) } @Composable @@ -122,6 +131,7 @@ fun lwaTypography( portrait = LwaTypography.Portrait( value = robotoMono.caption.copy( fontWeight = FontWeight.Bold, + fontSize = 14.sp, shadow = Shadow( color = Color.Black, offset = Offset(x = 1f, y = 1f), @@ -130,6 +140,7 @@ fun lwaTypography( ), separator = system.caption.copy( fontWeight = FontWeight.ExtraLight, + fontSize = 12.sp, shadow = Shadow( color = Color.Black, offset = Offset(x = 1f, y = 1f), @@ -138,6 +149,7 @@ fun lwaTypography( ), max = robotoMono.caption.copy( fontWeight = FontWeight.Light, + fontSize = 12.sp, shadow = Shadow( color = Color.Black, offset = Offset(x = 1f, y = 1f), @@ -193,7 +205,24 @@ fun lwaTypography( result = robotoMono.h4.copy( color = colors.base.onSurface, ), - ) + ), + debug = LwaTypography.Debug( + title = robotoMono.caption.copy( + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + lineHeight = 14.sp, + ), + propertyId = robotoMono.caption.copy( + fontWeight = FontWeight.Normal, + fontSize = 10.sp, + lineHeight = 14.sp, + ).toSpanStyle(), + propertyValue = robotoMono.caption.copy( + fontWeight = FontWeight.Normal, + fontSize = 12.sp, + lineHeight = 14.sp, + ), + ), ) } } diff --git a/server/src/main/kotlin/Module.kt b/server/src/main/kotlin/Module.kt index 2b0345d..9e4443a 100644 --- a/server/src/main/kotlin/Module.kt +++ b/server/src/main/kotlin/Module.kt @@ -1,3 +1,4 @@ +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 @@ -24,6 +25,7 @@ val serverModuleDependencies engineDependencies, storeDependencies, serviceDependencies, + logicsDependencies, ) val toolsDependencies @@ -60,3 +62,8 @@ val serviceDependencies singleOf(::ItemService) singleOf(::TagService) } + +val logicsDependencies + get() = module { + singleOf(::ItemUsageLogic) + } 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 new file mode 100644 index 0000000..094958e --- /dev/null +++ b/server/src/main/kotlin/com/pixelized/server/lwa/logics/ItemUsageLogic.kt @@ -0,0 +1,37 @@ +package com.pixelized.server.lwa.logics + +import com.pixelized.server.lwa.model.character.CharacterSheetService +import com.pixelized.server.lwa.model.inventory.InventoryService + +class ItemUsageLogic( + private val characterSheetService: CharacterSheetService, + private val inventoryService: InventoryService, +) { + suspend fun consumeInventoryItem( + characterSheetId: String, + inventoryId: String, + ): List { + val inventoryItem = inventoryService.getInventoryItem( + characterSheetId = characterSheetId, + inventoryId = inventoryId, + ) + val item = inventoryService.getItem( + itemId = inventoryItem.itemId, + ) + // equip the item form the inventory + inventoryService.consumeInventoryItem( + characterSheetId = characterSheetId, + inventoryId = inventoryId, + ) + // if consume didn't throw then add the alteration to the character + val alterations = item.alterations + alterations.forEach { alterationId -> + characterSheetService.updateAlteration( + characterSheetId = characterSheetId, + alterationId = alterationId, + active = true, + ) + } + return alterations + } +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/inventory/InventoryService.kt b/server/src/main/kotlin/com/pixelized/server/lwa/model/inventory/InventoryService.kt index 9c0ba11..e9538af 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/inventory/InventoryService.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/model/inventory/InventoryService.kt @@ -5,6 +5,7 @@ import com.pixelized.server.lwa.server.exception.BusinessException import com.pixelized.shared.lwa.model.inventory.Inventory import com.pixelized.shared.lwa.model.inventory.InventoryJson import com.pixelized.shared.lwa.model.inventory.factory.InventoryJsonFactory +import com.pixelized.shared.lwa.model.item.Item import com.pixelized.shared.lwa.protocol.rest.ApiPurseJson import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -106,6 +107,30 @@ class InventoryService( return inventoryId } + @Throws + suspend fun getInventoryItem( + characterSheetId: String, + inventoryId: String, + ): Inventory.Item { + // get the inventory of the character, if none create one. + val inventory = inventoryStore.inventoryFlow().value[characterSheetId] + ?: Inventory.empty(characterSheetId = characterSheetId) + // Guard case. + return inventory.items + .firstOrNull { it.inventoryId == inventoryId } + ?: throw BusinessException( + message = "InventoryItem (id:$inventoryId) not found in Inventory(characterSheetId:$characterSheetId).", + ) + } + + @Throws + suspend fun getItem( + itemId: String, + ): Item { + return itemStore.item(itemId = itemId) + ?: throw BusinessException(message = "Item (id:$itemId) not found.") + } + @Throws suspend fun changeInventoryItemCount( characterSheetId: String, 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 55810bc..88f90d0 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,5 +1,6 @@ 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 @@ -30,6 +31,7 @@ class Engine( val inventoryService: InventoryService, val tagService: TagService, val campaignJsonFactory: CampaignJsonFactory, + val itemUsageLogic: ItemUsageLogic, private val campaignStore: CampaignStore, private val characterStore: CharacterSheetStore, private val alterationStore: AlterationStore, diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/inventory/PUT_ConsumeInventoryItem.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/inventory/PUT_ConsumeInventoryItem.kt index 322c553..bb0e64f 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/inventory/PUT_ConsumeInventoryItem.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/inventory/PUT_ConsumeInventoryItem.kt @@ -6,6 +6,7 @@ import com.pixelized.server.lwa.utils.extentions.exception import com.pixelized.server.lwa.utils.extentions.inventoryId import com.pixelized.shared.lwa.protocol.rest.APIResponse import com.pixelized.shared.lwa.protocol.websocket.ApiSynchronisation +import com.pixelized.shared.lwa.protocol.websocket.CharacterSheetEvent import io.ktor.server.response.respond import io.ktor.server.routing.RoutingContext @@ -16,7 +17,7 @@ fun Engine.consumeInventoryItem(): suspend RoutingContext.() -> Unit { val characterSheetId = call.queryParameters.characterSheetId val inventoryId = call.queryParameters.inventoryId // add the item to the inventory. - inventoryService.consumeInventoryItem( + val alterationIds = itemUsageLogic.consumeInventoryItem( characterSheetId = characterSheetId, inventoryId = inventoryId, ) @@ -30,6 +31,16 @@ fun Engine.consumeInventoryItem(): suspend RoutingContext.() -> Unit { characterSheetId = characterSheetId, ), ) + alterationIds.forEach { + webSocket.emit( + value = CharacterSheetEvent.UpdateAlteration( + timestamp = System.currentTimeMillis(), + characterSheetId = characterSheetId, + alterationId = it, + active = true, + ) + ) + } } catch (exception: Exception) { call.exception( exception = exception,