From d52b9cdc755c6fb5fc5b072e78828be1f86e28a2 Mon Sep 17 00:00:00 2001 From: shahondin1624 Date: Fri, 13 Mar 2026 13:26:52 +0100 Subject: [PATCH] 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 --- .../kotlin/org/shahondin1624/App.kt | 3 + .../charactermodel/ResourcePanel.kt | 187 ++++++++++++++++++ 2 files changed, 190 insertions(+) create mode 100644 sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/charactermodel/ResourcePanel.kt diff --git a/sharedUI/src/commonMain/kotlin/org/shahondin1624/App.kt b/sharedUI/src/commonMain/kotlin/org/shahondin1624/App.kt index b6c40e7..3dbf12a 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.ResourcePanel import org.shahondin1624.lib.components.charactermodel.attributespage.AttributesPage import org.shahondin1624.lib.components.settings.SettingsPage import org.shahondin1624.model.EXAMPLE_CHARACTER @@ -308,6 +309,8 @@ private fun MainScaffold( ) { CharacterHeader(EXAMPLE_CHARACTER.characterData) Spacer(Modifier.height(contentPadding)) + ResourcePanel(EXAMPLE_CHARACTER.characterData, EXAMPLE_CHARACTER.attributes.edge) + Spacer(Modifier.height(contentPadding)) AttributesPage(EXAMPLE_CHARACTER) } } diff --git a/sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/charactermodel/ResourcePanel.kt b/sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/charactermodel/ResourcePanel.kt new file mode 100644 index 0000000..896b1a5 --- /dev/null +++ b/sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/charactermodel/ResourcePanel.kt @@ -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 + ) + } + } +}