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

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

View file

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

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

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

View file

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

View file

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

View file

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

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

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.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,

View file

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

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.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,