This commit was merged in pull request #61.
This commit is contained in:
@@ -11,6 +11,7 @@ import androidx.compose.ui.unit.Dp
|
|||||||
import org.shahondin1624.lib.components.UiConstants
|
import org.shahondin1624.lib.components.UiConstants
|
||||||
import org.shahondin1624.lib.components.charactermodel.attributespage.Attribute
|
import org.shahondin1624.lib.components.charactermodel.attributespage.Attribute
|
||||||
import org.shahondin1624.lib.components.charactermodel.attributespage.Talent
|
import org.shahondin1624.lib.components.charactermodel.attributespage.Talent
|
||||||
|
import org.shahondin1624.lib.functions.DiceRoll
|
||||||
import org.shahondin1624.model.charactermodel.ShadowrunCharacter
|
import org.shahondin1624.model.charactermodel.ShadowrunCharacter
|
||||||
import org.shahondin1624.theme.LocalWindowSizeClass
|
import org.shahondin1624.theme.LocalWindowSizeClass
|
||||||
import org.shahondin1624.theme.WindowSizeClass
|
import org.shahondin1624.theme.WindowSizeClass
|
||||||
@@ -35,6 +36,24 @@ fun CharacterSheetPage(character: ShadowrunCharacter, contentPadding: Dp) {
|
|||||||
var selectedTab by remember { mutableStateOf(CharacterTab.Overview) }
|
var selectedTab by remember { mutableStateOf(CharacterTab.Overview) }
|
||||||
val spacing = UiConstants.Spacing.medium(windowSizeClass)
|
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()) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
// Tab row
|
// Tab row
|
||||||
TabRow(
|
TabRow(
|
||||||
@@ -55,16 +74,16 @@ fun CharacterSheetPage(character: ShadowrunCharacter, contentPadding: Dp) {
|
|||||||
// Expanded: two-column layout for Overview and Combat
|
// Expanded: two-column layout for Overview and Combat
|
||||||
when (selectedTab) {
|
when (selectedTab) {
|
||||||
CharacterTab.Overview -> ExpandedOverviewContent(character, spacing)
|
CharacterTab.Overview -> ExpandedOverviewContent(character, spacing)
|
||||||
CharacterTab.Attributes -> AttributesContent(character, spacing)
|
CharacterTab.Attributes -> AttributesContent(character, spacing, onDiceRoll)
|
||||||
CharacterTab.Talents -> TalentsContent(character, spacing)
|
CharacterTab.Talents -> TalentsContent(character, spacing, onDiceRoll)
|
||||||
CharacterTab.Combat -> CombatContent(character, spacing)
|
CharacterTab.Combat -> CombatContent(character, spacing)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
when (selectedTab) {
|
when (selectedTab) {
|
||||||
CharacterTab.Overview -> OverviewContent(character, spacing)
|
CharacterTab.Overview -> OverviewContent(character, spacing)
|
||||||
CharacterTab.Attributes -> AttributesContent(character, spacing)
|
CharacterTab.Attributes -> AttributesContent(character, spacing, onDiceRoll)
|
||||||
CharacterTab.Talents -> TalentsContent(character, spacing)
|
CharacterTab.Talents -> TalentsContent(character, spacing, onDiceRoll)
|
||||||
CharacterTab.Combat -> CombatContent(character, spacing)
|
CharacterTab.Combat -> CombatContent(character, spacing)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -112,7 +131,11 @@ private fun ExpandedOverviewContent(character: ShadowrunCharacter, spacing: Dp)
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun AttributesContent(character: ShadowrunCharacter, spacing: Dp) {
|
private fun AttributesContent(
|
||||||
|
character: ShadowrunCharacter,
|
||||||
|
spacing: Dp,
|
||||||
|
onDiceRoll: (DiceRoll, String) -> Unit
|
||||||
|
) {
|
||||||
val windowSizeClass = LocalWindowSizeClass.current
|
val windowSizeClass = LocalWindowSizeClass.current
|
||||||
val columns = UiConstants.Grid.totalColumns(windowSizeClass)
|
val columns = UiConstants.Grid.totalColumns(windowSizeClass)
|
||||||
|
|
||||||
@@ -132,7 +155,10 @@ private fun AttributesContent(character: ShadowrunCharacter, spacing: Dp) {
|
|||||||
) {
|
) {
|
||||||
for (attr in row) {
|
for (attr in row) {
|
||||||
Box(modifier = Modifier.weight(1f)) {
|
Box(modifier = Modifier.weight(1f)) {
|
||||||
Attribute(attr)
|
Attribute(
|
||||||
|
attribute = attr,
|
||||||
|
onRoll = { roll -> onDiceRoll(roll, attr.type.name) }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
repeat(columns - row.size) {
|
repeat(columns - row.size) {
|
||||||
@@ -144,7 +170,11 @@ private fun AttributesContent(character: ShadowrunCharacter, spacing: Dp) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun TalentsContent(character: ShadowrunCharacter, spacing: Dp) {
|
private fun TalentsContent(
|
||||||
|
character: ShadowrunCharacter,
|
||||||
|
spacing: Dp,
|
||||||
|
onDiceRoll: (DiceRoll, String) -> Unit
|
||||||
|
) {
|
||||||
val windowSizeClass = LocalWindowSizeClass.current
|
val windowSizeClass = LocalWindowSizeClass.current
|
||||||
val totalCols = UiConstants.Grid.totalColumns(windowSizeClass)
|
val totalCols = UiConstants.Grid.totalColumns(windowSizeClass)
|
||||||
val talentSpan = UiConstants.Grid.talentSpan(windowSizeClass)
|
val talentSpan = UiConstants.Grid.talentSpan(windowSizeClass)
|
||||||
@@ -166,7 +196,11 @@ private fun TalentsContent(character: ShadowrunCharacter, spacing: Dp) {
|
|||||||
) {
|
) {
|
||||||
for (talent in row) {
|
for (talent in row) {
|
||||||
Box(modifier = Modifier.weight(1f)) {
|
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) {
|
repeat(talentsPerRow - row.size) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user