feat: add in-app dice roll results dialog (Closes #15) #61

Merged
shahondin1624 merged 1 commits from feature/issue-15-dice-roll-display into main 2026-03-13 13:38:34 +01:00
2 changed files with 186 additions and 8 deletions
Showing only changes of commit a8582ca89c - Show all commits

View File

@@ -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<DiceRoll?>(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) {

View File

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