From 7a32e4da2a695d2aa3089ed1cd0f6177d0226e5b Mon Sep 17 00:00:00 2001 From: shahondin1624 Date: Fri, 13 Mar 2026 13:30:20 +0100 Subject: [PATCH] feat: damage monitor display with visual box grids (Closes #13) (#58) --- .../kotlin/org/shahondin1624/App.kt | 3 + .../charactermodel/DamageMonitorPanel.kt | 145 ++++++++++++++++++ 2 files changed, 148 insertions(+) create mode 100644 sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/charactermodel/DamageMonitorPanel.kt diff --git a/sharedUI/src/commonMain/kotlin/org/shahondin1624/App.kt b/sharedUI/src/commonMain/kotlin/org/shahondin1624/App.kt index 0015371..fc3ce11 100644 --- a/sharedUI/src/commonMain/kotlin/org/shahondin1624/App.kt +++ b/sharedUI/src/commonMain/kotlin/org/shahondin1624/App.kt @@ -27,6 +27,7 @@ import org.jetbrains.compose.ui.tooling.preview.Preview import org.shahondin1624.lib.components.TestTags import org.shahondin1624.lib.components.UiConstants import org.shahondin1624.lib.components.charactermodel.CharacterHeader +import org.shahondin1624.lib.components.charactermodel.DamageMonitorPanel import org.shahondin1624.lib.components.charactermodel.DerivedAttributesPanel import org.shahondin1624.lib.components.charactermodel.ResourcePanel import org.shahondin1624.lib.components.charactermodel.attributespage.AttributesPage @@ -314,6 +315,8 @@ private fun MainScaffold( Spacer(Modifier.height(contentPadding)) DerivedAttributesPanel(EXAMPLE_CHARACTER.attributes) Spacer(Modifier.height(contentPadding)) + DamageMonitorPanel(EXAMPLE_CHARACTER.damageMonitor) + Spacer(Modifier.height(contentPadding)) AttributesPage(EXAMPLE_CHARACTER) } } diff --git a/sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/charactermodel/DamageMonitorPanel.kt b/sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/charactermodel/DamageMonitorPanel.kt new file mode 100644 index 0000000..4fc4e13 --- /dev/null +++ b/sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/charactermodel/DamageMonitorPanel.kt @@ -0,0 +1,145 @@ +package org.shahondin1624.lib.components.charactermodel + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.shahondin1624.lib.components.TestTags +import org.shahondin1624.lib.components.UiConstants +import org.shahondin1624.model.charactermodel.DamageMonitor +import org.shahondin1624.theme.LocalWindowSizeClass +import org.shahondin1624.theme.WindowSizeClass + +/** + * Displays the damage monitor as visual box grids for stun, physical, and overflow tracks. + * Read-only display; interactive damage marking is in story 6.5. + */ +@Composable +fun DamageMonitorPanel(damageMonitor: DamageMonitor) { + val windowSizeClass = LocalWindowSizeClass.current + val padding = UiConstants.Spacing.medium(windowSizeClass) + val spacing = UiConstants.Spacing.small(windowSizeClass) + + Card( + modifier = Modifier + .fillMaxWidth() + .testTag(TestTags.PANEL_DAMAGE_MONITOR) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(padding) + ) { + Text( + text = "Damage Monitor", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(bottom = spacing) + ) + + when (windowSizeClass) { + WindowSizeClass.Compact -> { + Column(verticalArrangement = Arrangement.spacedBy(spacing)) { + DamageTrack("Stun", damageMonitor.stunCurrent(), damageMonitor.stunMax()) + DamageTrack("Physical", damageMonitor.physicalCurrent(), damageMonitor.physicalMax()) + DamageTrack("Overflow", damageMonitor.overflowCurrent(), damageMonitor.overflowMax()) + } + } + WindowSizeClass.Medium, WindowSizeClass.Expanded -> { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(spacing) + ) { + DamageTrack("Stun", damageMonitor.stunCurrent(), damageMonitor.stunMax(), Modifier.weight(1f)) + DamageTrack("Physical", damageMonitor.physicalCurrent(), damageMonitor.physicalMax(), Modifier.weight(1f)) + DamageTrack("Overflow", damageMonitor.overflowCurrent(), damageMonitor.overflowMax(), Modifier.weight(1f)) + } + } + } + } + } +} + +@Composable +private fun DamageTrack( + label: String, + current: Int, + max: Int, + modifier: Modifier = Modifier +) { + val filledColor = MaterialTheme.colorScheme.error + val emptyColor = MaterialTheme.colorScheme.surfaceVariant + val borderColor = MaterialTheme.colorScheme.outline + + Column(modifier = modifier) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = label, + style = MaterialTheme.typography.labelMedium, + fontWeight = FontWeight.Medium + ) + Text( + text = "$current / $max", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + Spacer(Modifier.height(4.dp)) + + // Box grid with wound modifiers every 3 boxes + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(2.dp) + ) { + for (i in 1..max) { + val isFilled = i <= current + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.weight(1f) + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + .background( + color = if (isFilled) filledColor else emptyColor, + shape = MaterialTheme.shapes.extraSmall + ) + .border( + width = 1.dp, + color = borderColor, + shape = MaterialTheme.shapes.extraSmall + ) + ) + // Show wound modifier after every 3rd box + if (i % 3 == 0 && i < max) { + val woundLevel = i / 3 + Text( + text = "-$woundLevel", + style = MaterialTheme.typography.labelSmall.copy(fontSize = 8.sp), + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + } else { + // Empty spacer to maintain alignment + Spacer(Modifier.height(12.dp)) + } + } + } + } + } +}