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,