Compare commits

...
Sign in to create a new pull request.

4 commits

21 changed files with 1121 additions and 182 deletions

View file

@ -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>

View file

@ -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>

View file

@ -2,15 +2,32 @@ package com.pixelized.desktop.lwa.ui.composable.scene
import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.VectorConverter 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.Stable
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset 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.IntOffset
import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastRoundToInt 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 @Stable
class Camera( class Camera(
@ -98,4 +115,63 @@ class Camera(
else -> _zoom.animateTo(targetValue = initialZoom) 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())
},
)
}
}
} }

View file

@ -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())
},
)
}
}
}

View file

@ -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
)
}
}
}
}

View file

@ -1,9 +1,78 @@
package com.pixelized.desktop.lwa.ui.composable.scene package com.pixelized.desktop.lwa.ui.composable.scene
import androidx.compose.runtime.Stable 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.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 @Stable
data class FogOfWar( class FogOfWar(
val color: Color = Color.Black.copy(alpha = 0.5f), 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
}
}
}
}

View file

@ -2,12 +2,9 @@ package com.pixelized.desktop.lwa.ui.composable.scene
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Box 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.foundation.layout.padding
import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme
import androidx.compose.material.IconButton
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -17,6 +14,15 @@ import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset 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.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.PointerEvent import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.PointerEventType 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.layout.onSizeChanged
import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.toSize
import com.pixelized.desktop.lwa.ui.screen.campaign.LocalCampaignLayoutScope 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.CoroutineScope
import kotlinx.coroutines.launch 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.DrawableResource
import org.jetbrains.compose.resources.imageResource import org.jetbrains.compose.resources.imageResource
import org.jetbrains.compose.resources.painterResource import java.util.UUID
import kotlin.math.sign import kotlin.math.sign
@Stable @Stable
data class Scene( data class Scene(
val camera: Camera, val camera: Camera,
val layouts: List<Layout>,
val fogOfWar: FogOfWar, val fogOfWar: FogOfWar,
val layers: List<SceneLayer>,
val elements: List<SceneElement>,
) { ) {
val size: IntSize = IntSize( val size: IntSize = IntSize(
width = layouts.maxOf { it.size.width }, width = layers.maxOf { it.size.width },
height = layouts.maxOf { it.size.height }, height = layers.maxOf { it.size.height },
) )
} }
@Composable @Composable
fun Scene( fun Scene(
modifier: Modifier, modifier: Modifier = Modifier,
scene: Scene,
content: @Composable BoxScope.() -> Unit,
) { ) {
val campaign = LocalCampaignLayoutScope.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val scene = rememberScene( val cursors = remember {
camera = Camera( listOf(
initialZoom = 1f, Cursor()
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),
),
)
Box( Box(
modifier = modifier modifier = modifier
.graphicsLayer { clip = true } .graphicsLayer { clip = true }
.onCameraControl(scope = scope, scene = scene) .onCameraControl(scope = scope, scene = scene)
.drawScene(scene = scene) .onCursorControl(camera = scene.camera, cursor = cursors.first())
.fogOfWar(scene = scene) .drawLayers(camera = scene.camera, layers = scene.layers)
.drawElements(camera = scene.camera, elements = scene.elements)
.drawCursors(camera = scene.camera, cursors = cursors)
.drawFogOfWar(scene = scene),
) { ) {
Column( content()
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
)
}
}
}
}
@Composable SceneDebugPanel(
@Stable modifier = Modifier
fun rememberLayoutFromResource( .align(alignment = Alignment.TopEnd)
resource: DrawableResource, .padding(paddingValues = MaterialTheme.lwa.dimen.paddingValues),
offset: IntOffset = IntOffset.Zero, cursor = cursors.first(),
): Layout { scene = scene,
val texture = imageResource(
resource = resource,
)
return remember(resource) {
Layout(
texture = texture,
offset = offset,
) )
} }
} }
@Composable @Composable
@Stable @Stable
fun rememberScene( fun rememberLayoutFromResource(
camera: Camera, name: String,
fogOfWar: FogOfWar, resource: DrawableResource,
vararg layouts: Layout, offset: IntOffset = IntOffset.Zero,
): Scene { ): SceneLayer {
return remember { val texture = imageResource(
Scene( resource = resource,
camera = camera, )
layouts = layouts.toList(), return remember(resource) {
fogOfWar = fogOfWar, 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( @OptIn(ExperimentalComposeUiApi::class)
scene: Scene, 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 { ): Modifier = this.drawWithCache {
onDrawBehind { onDrawBehind {
scene.layouts.forEach { layout -> layers.forEach { layers ->
drawImage( drawImage(
image = layout.texture, image = layers.texture,
srcOffset = scene.camera.offset - layout.offset, srcOffset = camera.offset - layers.offset,
srcSize = scene.camera.cameraSizeZoomed, srcSize = camera.cameraSizeZoomed,
dstSize = scene.camera.cameraSize, dstSize = camera.cameraSize,
alpha = layout.alpha, alpha = layers.alpha,
) )
} }
} }
} }
fun Modifier.fogOfWar( fun Modifier.drawElements(
scene: Scene, camera: Camera,
elements: List<SceneElement>,
): Modifier = this.drawWithCache { ): Modifier = this.drawWithCache {
onDrawBehind { 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( private data class CursorDelta(
var lastDeltaTimestamp: Long = System.currentTimeMillis(), var lastDeltaTimestamp: Long = System.currentTimeMillis(),
var previousPosition: Offset = Offset.Zero, var previousPosition: Offset = Offset.Zero,

View file

@ -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())
},
)
}
}
}

View file

@ -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.Animatable
import androidx.compose.animation.core.VectorConverter import androidx.compose.animation.core.VectorConverter
import androidx.compose.runtime.Stable
import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.IntSize
@Stable open class SceneDrawable(
class Layout( val id: String,
val name: String,
val texture: ImageBitmap, val texture: ImageBitmap,
val offset: IntOffset = IntOffset.Zero, val offset: IntOffset,
val size: IntSize = IntSize(texture.width, texture.height), val size: IntSize,
private val initialAlpha: Float = 1f, private val initialAlpha: Float,
) { ) {
private val _alpha = Animatable( private val _alpha = Animatable(
initialValue = initialAlpha, initialValue = initialAlpha,
typeConverter = Float.VectorConverter, typeConverter = Float.Companion.VectorConverter,
) )
val alpha get() = _alpha.value val alpha get() = _alpha.value

View file

@ -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())
},
)
}
}
}

View file

@ -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())
},
)
}
}
}

View file

@ -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,
)

View file

@ -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.character.diminished.CharacterSheetDiminishedDialogViewModel
import com.pixelized.desktop.lwa.ui.composable.error.ErrorSnackHandler 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.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.LocalScreenController
import com.pixelized.desktop.lwa.ui.navigation.screen.destination.navigateToLevelScreen import com.pixelized.desktop.lwa.ui.navigation.screen.destination.navigateToLevelScreen
import com.pixelized.desktop.lwa.ui.overlay.portrait.PortraitOverlay import com.pixelized.desktop.lwa.ui.overlay.portrait.PortraitOverlay
@ -101,7 +101,7 @@ fun CampaignScreen(
}, },
main = { main = {
Scene( MapScene(
modifier = Modifier.matchParentSize(), modifier = Modifier.matchParentSize(),
) )
}, },

View file

@ -4,7 +4,6 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@ -15,8 +14,11 @@ import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.theme.lwa import com.pixelized.desktop.lwa.ui.theme.lwa
import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.Res
@ -37,6 +39,11 @@ fun CharacterRibbonStats(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
status: CharacterRibbonStatsUio?, 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 -> status?.let { status ->
Column( Column(
modifier = Modifier modifier = Modifier
@ -58,18 +65,14 @@ fun CharacterRibbonStats(
) )
Text( Text(
modifier = Modifier.alignByBaseline(), modifier = Modifier.alignByBaseline(),
style = MaterialTheme.lwa.typography.portrait.value, style = typography.portrait.value,
text = "${status.hp}", text = remember(status.hp, status.maxHp) {
) buildAnnotatedString {
Text( withStyle(style = valueSpanStyle) { append("${status.hp}") }
modifier = Modifier.alignByBaseline(), withStyle(style = separatorSpanStyle) { append("/") }
style = MaterialTheme.lwa.typography.portrait.separator, withStyle(style = maxSpanStyle) { append("${status.maxHp}") }
text = "/", }
) },
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.lwa.typography.portrait.max,
text = "${status.maxHp}",
) )
} }
Row { Row {
@ -84,19 +87,14 @@ fun CharacterRibbonStats(
modifier = Modifier.width(width = 2.dp), modifier = Modifier.width(width = 2.dp),
) )
Text( Text(
modifier = Modifier.alignByBaseline(), style = typography.portrait.value,
style = MaterialTheme.lwa.typography.portrait.value, text = remember(status.pp, status.maxPp) {
text = "${status.pp}", buildAnnotatedString {
) withStyle(style = valueSpanStyle) { append("${status.pp}") }
Text( withStyle(style = separatorSpanStyle) { append("/") }
modifier = Modifier.alignByBaseline(), withStyle(style = maxSpanStyle) { append("${status.maxPp}") }
style = MaterialTheme.lwa.typography.portrait.separator, }
text = "/", },
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.lwa.typography.portrait.max,
text = "${status.maxPp}",
) )
} }
} }

View file

@ -15,6 +15,7 @@ data class LwaDimen(
val layout: Layout, val layout: Layout,
val portrait: Portrait, val portrait: Portrait,
val sheet: Sheet, val sheet: Sheet,
val debug: Debug,
) { ) {
@Stable @Stable
data class Layout( data class Layout(
@ -33,6 +34,12 @@ data class LwaDimen(
val subCategory: Dp, val subCategory: Dp,
val characteristic: DpSize, val characteristic: DpSize,
) )
@Stable
data class Debug(
val panelWidth: Dp,
val offset: Dp,
)
} }
@Composable @Composable
@ -52,6 +59,10 @@ fun lwaDimen(
detailWidth = 128.dp * 4, detailWidth = 128.dp * 4,
chatMaxWidth = 600.dp, chatMaxWidth = 600.dp,
), ),
debug: LwaDimen.Debug = LwaDimen.Debug(
panelWidth = 200.dp,
offset = 8.dp,
)
): LwaDimen { ): LwaDimen {
return remember { return remember {
LwaDimen( LwaDimen(
@ -60,6 +71,7 @@ fun lwaDimen(
portrait = portrait, portrait = portrait,
sheet = sheet, sheet = sheet,
layout = layout, layout = layout,
debug = debug,
) )
} }
} }

View file

@ -10,6 +10,7 @@ import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.graphics.StrokeJoin import androidx.compose.ui.graphics.StrokeJoin
import androidx.compose.ui.graphics.drawscope.Fill import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
@ -30,6 +31,7 @@ data class LwaTypography(
val characterSheet: CharacterSheet, val characterSheet: CharacterSheet,
val inventory: Inventory, val inventory: Inventory,
val freeDiceThrow: Dice, val freeDiceThrow: Dice,
val debug: Debug,
) { ) {
@Stable @Stable
data class Chat( data class Chat(
@ -73,6 +75,13 @@ data class LwaTypography(
val dice: TextStyle, val dice: TextStyle,
val result: TextStyle, val result: TextStyle,
) )
@Stable
data class Debug(
val title: TextStyle,
val propertyId: SpanStyle,
val propertyValue: TextStyle,
)
} }
@Composable @Composable
@ -122,6 +131,7 @@ fun lwaTypography(
portrait = LwaTypography.Portrait( portrait = LwaTypography.Portrait(
value = robotoMono.caption.copy( value = robotoMono.caption.copy(
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
fontSize = 14.sp,
shadow = Shadow( shadow = Shadow(
color = Color.Black, color = Color.Black,
offset = Offset(x = 1f, y = 1f), offset = Offset(x = 1f, y = 1f),
@ -130,6 +140,7 @@ fun lwaTypography(
), ),
separator = system.caption.copy( separator = system.caption.copy(
fontWeight = FontWeight.ExtraLight, fontWeight = FontWeight.ExtraLight,
fontSize = 12.sp,
shadow = Shadow( shadow = Shadow(
color = Color.Black, color = Color.Black,
offset = Offset(x = 1f, y = 1f), offset = Offset(x = 1f, y = 1f),
@ -138,6 +149,7 @@ fun lwaTypography(
), ),
max = robotoMono.caption.copy( max = robotoMono.caption.copy(
fontWeight = FontWeight.Light, fontWeight = FontWeight.Light,
fontSize = 12.sp,
shadow = Shadow( shadow = Shadow(
color = Color.Black, color = Color.Black,
offset = Offset(x = 1f, y = 1f), offset = Offset(x = 1f, y = 1f),
@ -193,7 +205,24 @@ fun lwaTypography(
result = robotoMono.h4.copy( result = robotoMono.h4.copy(
color = colors.base.onSurface, 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,
),
),
) )
} }
} }

View file

@ -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.AlterationService
import com.pixelized.server.lwa.model.alteration.AlterationStore import com.pixelized.server.lwa.model.alteration.AlterationStore
import com.pixelized.server.lwa.model.campaign.CampaignService import com.pixelized.server.lwa.model.campaign.CampaignService
@ -24,6 +25,7 @@ val serverModuleDependencies
engineDependencies, engineDependencies,
storeDependencies, storeDependencies,
serviceDependencies, serviceDependencies,
logicsDependencies,
) )
val toolsDependencies val toolsDependencies
@ -60,3 +62,8 @@ val serviceDependencies
singleOf(::ItemService) singleOf(::ItemService)
singleOf(::TagService) singleOf(::TagService)
} }
val logicsDependencies
get() = module {
singleOf(::ItemUsageLogic)
}

View file

@ -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
}
}

View file

@ -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.Inventory
import com.pixelized.shared.lwa.model.inventory.InventoryJson import com.pixelized.shared.lwa.model.inventory.InventoryJson
import com.pixelized.shared.lwa.model.inventory.factory.InventoryJsonFactory 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 com.pixelized.shared.lwa.protocol.rest.ApiPurseJson
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -106,6 +107,30 @@ class InventoryService(
return inventoryId 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 @Throws
suspend fun changeInventoryItemCount( suspend fun changeInventoryItemCount(
characterSheetId: String, characterSheetId: String,

View file

@ -1,5 +1,6 @@
package com.pixelized.server.lwa.server 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.AlterationService
import com.pixelized.server.lwa.model.alteration.AlterationStore import com.pixelized.server.lwa.model.alteration.AlterationStore
import com.pixelized.server.lwa.model.campaign.CampaignService import com.pixelized.server.lwa.model.campaign.CampaignService
@ -30,6 +31,7 @@ class Engine(
val inventoryService: InventoryService, val inventoryService: InventoryService,
val tagService: TagService, val tagService: TagService,
val campaignJsonFactory: CampaignJsonFactory, val campaignJsonFactory: CampaignJsonFactory,
val itemUsageLogic: ItemUsageLogic,
private val campaignStore: CampaignStore, private val campaignStore: CampaignStore,
private val characterStore: CharacterSheetStore, private val characterStore: CharacterSheetStore,
private val alterationStore: AlterationStore, private val alterationStore: AlterationStore,

View file

@ -6,6 +6,7 @@ import com.pixelized.server.lwa.utils.extentions.exception
import com.pixelized.server.lwa.utils.extentions.inventoryId import com.pixelized.server.lwa.utils.extentions.inventoryId
import com.pixelized.shared.lwa.protocol.rest.APIResponse import com.pixelized.shared.lwa.protocol.rest.APIResponse
import com.pixelized.shared.lwa.protocol.websocket.ApiSynchronisation 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.response.respond
import io.ktor.server.routing.RoutingContext import io.ktor.server.routing.RoutingContext
@ -16,7 +17,7 @@ fun Engine.consumeInventoryItem(): suspend RoutingContext.() -> Unit {
val characterSheetId = call.queryParameters.characterSheetId val characterSheetId = call.queryParameters.characterSheetId
val inventoryId = call.queryParameters.inventoryId val inventoryId = call.queryParameters.inventoryId
// add the item to the inventory. // add the item to the inventory.
inventoryService.consumeInventoryItem( val alterationIds = itemUsageLogic.consumeInventoryItem(
characterSheetId = characterSheetId, characterSheetId = characterSheetId,
inventoryId = inventoryId, inventoryId = inventoryId,
) )
@ -30,6 +31,16 @@ fun Engine.consumeInventoryItem(): suspend RoutingContext.() -> Unit {
characterSheetId = characterSheetId, characterSheetId = characterSheetId,
), ),
) )
alterationIds.forEach {
webSocket.emit(
value = CharacterSheetEvent.UpdateAlteration(
timestamp = System.currentTimeMillis(),
characterSheetId = characterSheetId,
alterationId = it,
active = true,
)
)
}
} catch (exception: Exception) { } catch (exception: Exception) {
call.exception( call.exception(
exception = exception, exception = exception,