feat: resource trackers for karma, nuyen, essence, edge (Closes #10)

Add ResourcePanel composable showing current/total karma, nuyen,
essence (1 decimal), and Edge. Compact uses 2x2 grid, Medium/Expanded
uses single row. Edge uses tertiary container color with star icon
for visual distinction. Placed below character header.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
shahondin1624
2026-03-13 13:26:52 +01:00
parent 6af4120959
commit d52b9cdc75
2 changed files with 190 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.TestTags
import org.shahondin1624.lib.components.UiConstants import org.shahondin1624.lib.components.UiConstants
import org.shahondin1624.lib.components.charactermodel.CharacterHeader import org.shahondin1624.lib.components.charactermodel.CharacterHeader
import org.shahondin1624.lib.components.charactermodel.ResourcePanel
import org.shahondin1624.lib.components.charactermodel.attributespage.AttributesPage import org.shahondin1624.lib.components.charactermodel.attributespage.AttributesPage
import org.shahondin1624.lib.components.settings.SettingsPage import org.shahondin1624.lib.components.settings.SettingsPage
import org.shahondin1624.model.EXAMPLE_CHARACTER import org.shahondin1624.model.EXAMPLE_CHARACTER
@@ -308,6 +309,8 @@ private fun MainScaffold(
) { ) {
CharacterHeader(EXAMPLE_CHARACTER.characterData) CharacterHeader(EXAMPLE_CHARACTER.characterData)
Spacer(Modifier.height(contentPadding)) Spacer(Modifier.height(contentPadding))
ResourcePanel(EXAMPLE_CHARACTER.characterData, EXAMPLE_CHARACTER.attributes.edge)
Spacer(Modifier.height(contentPadding))
AttributesPage(EXAMPLE_CHARACTER) AttributesPage(EXAMPLE_CHARACTER)
} }
} }

View File

@@ -0,0 +1,187 @@
package org.shahondin1624.lib.components.charactermodel
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Star
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.unit.dp
import org.shahondin1624.lib.components.TestTags
import org.shahondin1624.lib.components.UiConstants
import org.shahondin1624.model.characterdata.CharacterData
import org.shahondin1624.theme.LocalWindowSizeClass
import org.shahondin1624.theme.WindowSizeClass
import kotlin.math.roundToInt
/**
* Formats a float to one decimal place without platform-specific String.format.
*/
private fun formatEssence(value: Float): String {
val rounded = (value * 10).roundToInt() / 10.0
val intPart = rounded.toInt()
val decPart = ((rounded - intPart) * 10).roundToInt()
return "$intPart.$decPart"
}
/**
* Displays resource trackers: Karma (current/total), Nuyen, Essence, and Edge.
* Compact: 2x2 grid layout. Expanded: single row.
*/
@Composable
fun ResourcePanel(characterData: CharacterData, edge: Int) {
val windowSizeClass = LocalWindowSizeClass.current
val spacing = UiConstants.Spacing.small(windowSizeClass)
Card(
modifier = Modifier
.fillMaxWidth()
.testTag(TestTags.PANEL_RESOURCES)
) {
when (windowSizeClass) {
WindowSizeClass.Compact -> CompactResources(characterData, edge, spacing)
WindowSizeClass.Medium, WindowSizeClass.Expanded -> ExpandedResources(characterData, edge, spacing)
}
}
}
@Composable
private fun CompactResources(characterData: CharacterData, edge: Int, spacing: androidx.compose.ui.unit.Dp) {
val padding = UiConstants.Spacing.medium(LocalWindowSizeClass.current)
Column(
modifier = Modifier
.fillMaxWidth()
.padding(padding),
verticalArrangement = Arrangement.spacedBy(spacing)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(spacing)
) {
ResourceItem(
label = "Karma",
value = "${characterData.currentKarma} / ${characterData.totalKarma}",
modifier = Modifier.weight(1f)
)
ResourceItem(
label = "Nuyen",
value = "${characterData.nuyen}\u00A5",
modifier = Modifier.weight(1f)
)
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(spacing)
) {
ResourceItem(
label = "Essence",
value = formatEssence(characterData.essence),
modifier = Modifier.weight(1f)
)
EdgeItem(
edge = edge,
modifier = Modifier.weight(1f)
)
}
}
}
@Composable
private fun ExpandedResources(characterData: CharacterData, edge: Int, spacing: androidx.compose.ui.unit.Dp) {
val padding = UiConstants.Spacing.medium(LocalWindowSizeClass.current)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(padding),
horizontalArrangement = Arrangement.spacedBy(spacing),
verticalAlignment = Alignment.CenterVertically
) {
ResourceItem(
label = "Karma",
value = "${characterData.currentKarma} / ${characterData.totalKarma}",
modifier = Modifier.weight(1f)
)
ResourceItem(
label = "Nuyen",
value = "${characterData.nuyen}\u00A5",
modifier = Modifier.weight(1f)
)
ResourceItem(
label = "Essence",
value = formatEssence(characterData.essence),
modifier = Modifier.weight(1f)
)
EdgeItem(
edge = edge,
modifier = Modifier.weight(1f)
)
}
}
@Composable
private fun ResourceItem(label: String, value: String, modifier: Modifier = Modifier) {
Surface(
modifier = modifier,
shape = MaterialTheme.shapes.small,
color = MaterialTheme.colorScheme.surfaceVariant
) {
Column(
modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = label,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = value,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
}
}
}
/**
* Edge resource item with distinct visual treatment (tertiary color).
*/
@Composable
private fun EdgeItem(edge: Int, modifier: Modifier = Modifier) {
Surface(
modifier = modifier,
shape = MaterialTheme.shapes.small,
color = MaterialTheme.colorScheme.tertiaryContainer
) {
Column(
modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
Icon(
imageVector = Icons.Default.Star,
contentDescription = null,
modifier = Modifier.size(14.dp),
tint = MaterialTheme.colorScheme.onTertiaryContainer
)
Text(
text = "Edge",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onTertiaryContainer
)
}
Text(
text = edge.toString(),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onTertiaryContainer
)
}
}
}