Compare commits
4 commits
feature/la
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d5f29c18c | |||
| ae2c3da582 | |||
| 4e013aa358 | |||
| 6e1aaa10d3 |
21 changed files with 1121 additions and 182 deletions
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="M720,760Q703,760 691.5,748.5Q680,737 680,720Q680,703 691.5,691.5Q703,680 720,680Q737,680 748.5,691.5Q760,703 760,720Q760,737 748.5,748.5Q737,760 720,760ZM280,880Q263,880 251.5,868.5Q240,857 240,840Q240,823 251.5,811.5Q263,800 280,800Q297,800 308.5,811.5Q320,823 320,840Q320,857 308.5,868.5Q297,880 280,880ZM240,760Q223,760 211.5,748.5Q200,737 200,720Q200,703 211.5,691.5Q223,680 240,680L600,680Q617,680 628.5,691.5Q640,703 640,720Q640,737 628.5,748.5Q617,760 600,760L240,760ZM400,880Q383,880 371.5,868.5Q360,857 360,840Q360,823 371.5,811.5Q383,800 400,800L680,800Q697,800 708.5,811.5Q720,823 720,840Q720,857 708.5,868.5Q697,880 680,880L400,880ZM300,640Q209,640 144.5,575.5Q80,511 80,420Q80,337 135,275Q190,213 271,202Q303,145 358.5,112.5Q414,80 480,80Q570,80 636.5,137.5Q703,195 717,281Q786,287 833,338Q880,389 880,460Q880,535 827.5,587.5Q775,640 700,640L300,640ZM300,560L700,560Q742,560 771,531Q800,502 800,460Q800,418 771,389Q742,360 700,360L640,360L640,320Q640,254 593,207Q546,160 480,160Q432,160 392.5,186Q353,212 333,256L323,280L298,280Q241,282 200.5,322.5Q160,363 160,420Q160,478 201,519Q242,560 300,560ZM480,360L480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360L480,360L480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360L480,360L480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360Z" />
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="M720,760Q703,760 691.5,748.5Q680,737 680,720Q680,703 691.5,691.5Q703,680 720,680Q737,680 748.5,691.5Q760,703 760,720Q760,737 748.5,748.5Q737,760 720,760ZM280,880Q263,880 251.5,868.5Q240,857 240,840Q240,823 251.5,811.5Q263,800 280,800Q297,800 308.5,811.5Q320,823 320,840Q320,857 308.5,868.5Q297,880 280,880ZM240,760Q223,760 211.5,748.5Q200,737 200,720Q200,703 211.5,691.5Q223,680 240,680L600,680Q617,680 628.5,691.5Q640,703 640,720Q640,737 628.5,748.5Q617,760 600,760L240,760ZM400,880Q383,880 371.5,868.5Q360,857 360,840Q360,823 371.5,811.5Q383,800 400,800L680,800Q697,800 708.5,811.5Q720,823 720,840Q720,857 708.5,868.5Q697,880 680,880L400,880ZM300,640Q209,640 144.5,575.5Q80,511 80,420Q80,337 135,275Q190,213 271,202Q303,145 358.5,112.5Q414,80 480,80Q570,80 636.5,137.5Q703,195 717,281Q786,287 833,338Q880,389 880,460Q880,535 827.5,587.5Q775,640 700,640L300,640Z" />
|
||||
</vector>
|
||||
|
|
@ -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())
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
) {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Layout>,
|
||||
val fogOfWar: FogOfWar,
|
||||
val layers: List<SceneLayer>,
|
||||
val elements: List<SceneElement>,
|
||||
) {
|
||||
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<Cursor>,
|
||||
): 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<SceneLayer>,
|
||||
): 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<SceneElement>,
|
||||
): 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,
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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())
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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(),
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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}") }
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue