diff --git a/sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/charactermodel/CharacterSheetPage.kt b/sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/charactermodel/CharacterSheetPage.kt index ecb1f18..f409ae2 100644 --- a/sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/charactermodel/CharacterSheetPage.kt +++ b/sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/charactermodel/CharacterSheetPage.kt @@ -11,6 +11,7 @@ import androidx.compose.ui.unit.Dp import org.shahondin1624.lib.components.UiConstants import org.shahondin1624.lib.components.charactermodel.attributespage.Attribute import org.shahondin1624.lib.components.charactermodel.attributespage.Talent +import org.shahondin1624.lib.functions.DiceRoll import org.shahondin1624.model.charactermodel.ShadowrunCharacter import org.shahondin1624.theme.LocalWindowSizeClass import org.shahondin1624.theme.WindowSizeClass @@ -35,6 +36,24 @@ fun CharacterSheetPage(character: ShadowrunCharacter, contentPadding: Dp) { var selectedTab by remember { mutableStateOf(CharacterTab.Overview) } val spacing = UiConstants.Spacing.medium(windowSizeClass) + // Dice roll dialog state + var pendingRoll by remember { mutableStateOf(null) } + var pendingRollLabel by remember { mutableStateOf("") } + + val onDiceRoll: (DiceRoll, String) -> Unit = { roll, label -> + pendingRoll = roll + pendingRollLabel = label + } + + // Show dice roll result dialog + pendingRoll?.let { roll -> + DiceRollResultDialog( + diceRoll = roll, + rollLabel = pendingRollLabel, + onDismiss = { pendingRoll = null } + ) + } + Column(modifier = Modifier.fillMaxSize()) { // Tab row TabRow( @@ -55,16 +74,16 @@ fun CharacterSheetPage(character: ShadowrunCharacter, contentPadding: Dp) { // Expanded: two-column layout for Overview and Combat when (selectedTab) { CharacterTab.Overview -> ExpandedOverviewContent(character, spacing) - CharacterTab.Attributes -> AttributesContent(character, spacing) - CharacterTab.Talents -> TalentsContent(character, spacing) + CharacterTab.Attributes -> AttributesContent(character, spacing, onDiceRoll) + CharacterTab.Talents -> TalentsContent(character, spacing, onDiceRoll) CharacterTab.Combat -> CombatContent(character, spacing) } } else -> { when (selectedTab) { CharacterTab.Overview -> OverviewContent(character, spacing) - CharacterTab.Attributes -> AttributesContent(character, spacing) - CharacterTab.Talents -> TalentsContent(character, spacing) + CharacterTab.Attributes -> AttributesContent(character, spacing, onDiceRoll) + CharacterTab.Talents -> TalentsContent(character, spacing, onDiceRoll) CharacterTab.Combat -> CombatContent(character, spacing) } } @@ -112,7 +131,11 @@ private fun ExpandedOverviewContent(character: ShadowrunCharacter, spacing: Dp) } @Composable -private fun AttributesContent(character: ShadowrunCharacter, spacing: Dp) { +private fun AttributesContent( + character: ShadowrunCharacter, + spacing: Dp, + onDiceRoll: (DiceRoll, String) -> Unit +) { val windowSizeClass = LocalWindowSizeClass.current val columns = UiConstants.Grid.totalColumns(windowSizeClass) @@ -132,7 +155,10 @@ private fun AttributesContent(character: ShadowrunCharacter, spacing: Dp) { ) { for (attr in row) { Box(modifier = Modifier.weight(1f)) { - Attribute(attr) + Attribute( + attribute = attr, + onRoll = { roll -> onDiceRoll(roll, attr.type.name) } + ) } } repeat(columns - row.size) { @@ -144,7 +170,11 @@ private fun AttributesContent(character: ShadowrunCharacter, spacing: Dp) { } @Composable -private fun TalentsContent(character: ShadowrunCharacter, spacing: Dp) { +private fun TalentsContent( + character: ShadowrunCharacter, + spacing: Dp, + onDiceRoll: (DiceRoll, String) -> Unit +) { val windowSizeClass = LocalWindowSizeClass.current val totalCols = UiConstants.Grid.totalColumns(windowSizeClass) val talentSpan = UiConstants.Grid.talentSpan(windowSizeClass) @@ -166,7 +196,11 @@ private fun TalentsContent(character: ShadowrunCharacter, spacing: Dp) { ) { for (talent in row) { Box(modifier = Modifier.weight(1f)) { - Talent(talent, character.attributes) + Talent( + talent = talent, + attributes = character.attributes, + onRoll = { roll -> onDiceRoll(roll, talent.name) } + ) } } repeat(talentsPerRow - row.size) { diff --git a/sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/charactermodel/DiceRollResultDialog.kt b/sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/charactermodel/DiceRollResultDialog.kt new file mode 100644 index 0000000..eafbe88 --- /dev/null +++ b/sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/charactermodel/DiceRollResultDialog.kt @@ -0,0 +1,144 @@ +package org.shahondin1624.lib.components.charactermodel + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import org.shahondin1624.lib.functions.DiceRoll + +/** + * Dialog displaying dice roll results with individual die values. + * Successes (5+) highlighted in green, ones highlighted in red. + */ +@Composable +fun DiceRollResultDialog( + diceRoll: DiceRoll, + rollLabel: String, + onDismiss: () -> Unit +) { + val ones = diceRoll.result.count { it == 1 } + val isGlitch = ones > diceRoll.numberOfDice / 2 + val isCriticalGlitch = isGlitch && diceRoll.numberOfSuccesses == 0 + + AlertDialog( + onDismissRequest = onDismiss, + title = { + Text( + text = rollLabel, + style = MaterialTheme.typography.titleLarge + ) + }, + text = { + Column( + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + // Summary row + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = "${diceRoll.numberOfDice} dice rolled", + style = MaterialTheme.typography.bodyMedium + ) + Text( + text = "${diceRoll.numberOfSuccesses} successes", + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Bold, + color = if (diceRoll.numberOfSuccesses > 0) + Color(0xFF4CAF50) else MaterialTheme.colorScheme.onSurface + ) + } + + // Individual dice results + DiceResultGrid(diceRoll.result) + + // Glitch detection + if (isCriticalGlitch) { + Surface( + color = MaterialTheme.colorScheme.errorContainer, + shape = MaterialTheme.shapes.small + ) { + Text( + text = "CRITICAL GLITCH!", + modifier = Modifier.fillMaxWidth().padding(8.dp), + textAlign = TextAlign.Center, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onErrorContainer, + style = MaterialTheme.typography.titleMedium + ) + } + } else if (isGlitch) { + Surface( + color = MaterialTheme.colorScheme.errorContainer, + shape = MaterialTheme.shapes.small + ) { + Text( + text = "Glitch! ($ones ones out of ${diceRoll.numberOfDice})", + modifier = Modifier.fillMaxWidth().padding(8.dp), + textAlign = TextAlign.Center, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onErrorContainer + ) + } + } + } + }, + confirmButton = { + TextButton(onClick = onDismiss) { + Text("OK") + } + } + ) +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun DiceResultGrid(results: List) { + FlowRow( + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + for (value in results) { + DieChip(value) + } + } +} + +@Composable +private fun DieChip(value: Int) { + val isSuccess = value >= 5 + val isOne = value == 1 + + val backgroundColor = when { + isSuccess -> Color(0xFF4CAF50) + isOne -> MaterialTheme.colorScheme.error + else -> MaterialTheme.colorScheme.surfaceVariant + } + val textColor = when { + isSuccess -> Color.White + isOne -> MaterialTheme.colorScheme.onError + else -> MaterialTheme.colorScheme.onSurfaceVariant + } + + Box( + modifier = Modifier + .size(32.dp) + .background(backgroundColor, CircleShape), + contentAlignment = Alignment.Center + ) { + Text( + text = value.toString(), + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Bold, + color = textColor + ) + } +}