feat: damage monitor display with visual box grids (Closes #13) (#58)

This commit was merged in pull request #58.
This commit is contained in:
2026-03-13 13:30:20 +01:00
parent c3b7fca754
commit 7a32e4da2a
2 changed files with 148 additions and 0 deletions

View File

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

View File

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