From d12269cdb56fcbcbe73f2f0fbc65f2d88039b2f3 Mon Sep 17 00:00:00 2001 From: shahondin1624 Date: Fri, 13 Mar 2026 13:50:18 +0100 Subject: [PATCH] feat: add editable talent ratings with validation dialog (Closes #25) Add TalentEditDialog with rating input (0-12, 0 = untrained) and validation. Talent cards now open edit dialog on tap. Dice pool display updates immediately through CharacterViewModel. Add withTalentRating() to Talents model for immutable talent rating updates. Co-Authored-By: Claude Opus 4.6 --- .../shahondin1624/lib/components/TestTags.kt | 7 ++ .../charactermodel/CharacterSheetPage.kt | 33 ++++++- .../charactermodel/attributespage/Talent.kt | 11 ++- .../attributespage/TalentEditDialog.kt | 88 +++++++++++++++++++ .../shahondin1624/model/talents/Talents.kt | 22 ++++- 5 files changed, 155 insertions(+), 6 deletions(-) create mode 100644 sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/charactermodel/attributespage/TalentEditDialog.kt diff --git a/sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/TestTags.kt b/sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/TestTags.kt index b95b172..ad763c5 100644 --- a/sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/TestTags.kt +++ b/sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/TestTags.kt @@ -37,6 +37,13 @@ object TestTags { const val ATTRIBUTE_EDIT_DISMISS = "attribute_edit_dismiss" const val ATTRIBUTE_EDIT_ERROR = "attribute_edit_error" + // --- Talent edit dialog --- + const val TALENT_EDIT_DIALOG = "talent_edit_dialog" + const val TALENT_EDIT_INPUT = "talent_edit_input" + const val TALENT_EDIT_CONFIRM = "talent_edit_confirm" + const val TALENT_EDIT_DISMISS = "talent_edit_dismiss" + const val TALENT_EDIT_ERROR = "talent_edit_error" + // --- Dice roll result dialog --- const val DICE_ROLL_DIALOG = "dice_roll_dialog" const val DICE_ROLL_DICE_COUNT = "dice_roll_dice_count" 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 9ded6bd..610a195 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 @@ -12,6 +12,8 @@ import org.shahondin1624.lib.components.UiConstants import org.shahondin1624.lib.components.charactermodel.attributespage.Attribute import org.shahondin1624.lib.components.charactermodel.attributespage.AttributeEditDialog import org.shahondin1624.lib.components.charactermodel.attributespage.Talent +import org.shahondin1624.lib.components.charactermodel.attributespage.TalentEditDialog +import org.shahondin1624.model.talents.TalentDefinition import org.shahondin1624.lib.functions.DiceRoll import org.shahondin1624.model.attributes.AttributeType import org.shahondin1624.model.charactermodel.ShadowrunCharacter @@ -54,6 +56,9 @@ fun CharacterSheetPage( // Attribute edit dialog state var editingAttributeType by remember { mutableStateOf(null) } + // Talent edit dialog state + var editingTalent by remember { mutableStateOf(null) } + // Show dice roll result dialog pendingRoll?.let { roll -> DiceRollResultDialog( @@ -79,6 +84,21 @@ fun CharacterSheetPage( ) } + // Show talent edit dialog + editingTalent?.let { talent -> + TalentEditDialog( + talent = talent, + onConfirm = { newValue -> + onUpdateCharacter { char -> + val newTalents = char.talents.withTalentRating(talent.name, newValue) + char.copy(talents = newTalents) + } + editingTalent = null + }, + onDismiss = { editingTalent = null } + ) + } + Column(modifier = Modifier.fillMaxSize()) { // Tab row TabRow( @@ -97,6 +117,9 @@ fun CharacterSheetPage( val onEditAttribute: (AttributeType) -> Unit = { type -> editingAttributeType = type } + val onEditTalent: (TalentDefinition) -> Unit = { talent -> + editingTalent = talent + } when (windowSizeClass) { WindowSizeClass.Expanded -> { @@ -104,7 +127,7 @@ fun CharacterSheetPage( when (selectedTab) { CharacterTab.Overview -> ExpandedOverviewContent(character, spacing) CharacterTab.Attributes -> AttributesContent(character, spacing, onDiceRoll, onEditAttribute) - CharacterTab.Talents -> TalentsContent(character, spacing, onDiceRoll) + CharacterTab.Talents -> TalentsContent(character, spacing, onDiceRoll, onEditTalent) CharacterTab.Combat -> CombatContent(character, spacing) } } @@ -112,7 +135,7 @@ fun CharacterSheetPage( when (selectedTab) { CharacterTab.Overview -> OverviewContent(character, spacing) CharacterTab.Attributes -> AttributesContent(character, spacing, onDiceRoll, onEditAttribute) - CharacterTab.Talents -> TalentsContent(character, spacing, onDiceRoll) + CharacterTab.Talents -> TalentsContent(character, spacing, onDiceRoll, onEditTalent) CharacterTab.Combat -> CombatContent(character, spacing) } } @@ -204,7 +227,8 @@ private fun AttributesContent( private fun TalentsContent( character: ShadowrunCharacter, spacing: Dp, - onDiceRoll: (DiceRoll, String) -> Unit + onDiceRoll: (DiceRoll, String) -> Unit, + onEditTalent: (TalentDefinition) -> Unit ) { val windowSizeClass = LocalWindowSizeClass.current val totalCols = UiConstants.Grid.totalColumns(windowSizeClass) @@ -230,7 +254,8 @@ private fun TalentsContent( Talent( talent = talent, attributes = character.attributes, - onRoll = { roll -> onDiceRoll(roll, talent.name) } + onRoll = { roll -> onDiceRoll(roll, talent.name) }, + onEdit = { onEditTalent(talent) } ) } } diff --git a/sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/charactermodel/attributespage/Talent.kt b/sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/charactermodel/attributespage/Talent.kt index 3e090ea..7a9cc1e 100644 --- a/sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/charactermodel/attributespage/Talent.kt +++ b/sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/charactermodel/attributespage/Talent.kt @@ -2,6 +2,7 @@ package org.shahondin1624.lib.components.charactermodel.attributespage import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth @@ -42,10 +43,18 @@ fun Talent( onRoll: (DiceRoll) -> Unit = { println("Result for $talent: ${it.result}") }, + onEdit: (() -> Unit)? = null, ) { var isInDarkMode by LocalThemeIsDark.current val textColor = if (isInDarkMode) Color.Black else Color.White - Card(modifier = Modifier.testTag(TestTags.talentCard(talent.name))) { + Card( + modifier = Modifier + .testTag(TestTags.talentCard(talent.name)) + .then( + if (onEdit != null) Modifier.clickable { onEdit() } + else Modifier + ) + ) { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start, diff --git a/sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/charactermodel/attributespage/TalentEditDialog.kt b/sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/charactermodel/attributespage/TalentEditDialog.kt new file mode 100644 index 0000000..a819fb9 --- /dev/null +++ b/sharedUI/src/commonMain/kotlin/org/shahondin1624/lib/components/charactermodel/attributespage/TalentEditDialog.kt @@ -0,0 +1,88 @@ +package org.shahondin1624.lib.components.charactermodel.attributespage + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import org.shahondin1624.lib.components.TestTags +import org.shahondin1624.model.talents.TalentDefinition + +/** + * Dialog for editing a talent's rating. + * Rating range is 0-12; 0 means untrained. + */ +@Composable +fun TalentEditDialog( + talent: TalentDefinition, + maxValue: Int = 12, + onConfirm: (Int) -> Unit, + onDismiss: () -> Unit +) { + var textValue by remember { mutableStateOf(talent.value.toString()) } + val parsedValue = textValue.toIntOrNull() + val isValid = parsedValue != null && parsedValue in 0..maxValue + + AlertDialog( + onDismissRequest = onDismiss, + modifier = Modifier.testTag(TestTags.TALENT_EDIT_DIALOG), + title = { + Text(text = "Edit ${talent.name}") + }, + text = { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + OutlinedTextField( + value = textValue, + onValueChange = { newValue -> + if (newValue.all { it.isDigit() } || newValue.isEmpty()) { + textValue = newValue + } + }, + label = { Text("Rating (0-$maxValue, 0 = untrained)") }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + isError = !isValid && textValue.isNotEmpty(), + singleLine = true, + modifier = Modifier + .fillMaxWidth() + .testTag(TestTags.TALENT_EDIT_INPUT) + ) + if (!isValid && textValue.isNotEmpty()) { + Text( + text = if (parsedValue != null && parsedValue < 0) { + "Minimum rating is 0" + } else if (parsedValue != null && parsedValue > maxValue) { + "Maximum rating is $maxValue" + } else { + "Enter a valid number" + }, + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.testTag(TestTags.TALENT_EDIT_ERROR) + ) + } + } + }, + confirmButton = { + TextButton( + onClick = { if (isValid) onConfirm(parsedValue!!) }, + enabled = isValid, + modifier = Modifier.testTag(TestTags.TALENT_EDIT_CONFIRM) + ) { + Text("Save") + } + }, + dismissButton = { + TextButton( + onClick = onDismiss, + modifier = Modifier.testTag(TestTags.TALENT_EDIT_DISMISS) + ) { + Text("Cancel") + } + } + ) +} diff --git a/sharedUI/src/commonMain/kotlin/org/shahondin1624/model/talents/Talents.kt b/sharedUI/src/commonMain/kotlin/org/shahondin1624/model/talents/Talents.kt index 5af4b79..b7b5029 100644 --- a/sharedUI/src/commonMain/kotlin/org/shahondin1624/model/talents/Talents.kt +++ b/sharedUI/src/commonMain/kotlin/org/shahondin1624/model/talents/Talents.kt @@ -7,4 +7,24 @@ import org.shahondin1624.model.Versionable data class Talents( val talents: List = createAllProvidedTalents(), override val version: String = "v0.1" -) : Versionable +) : Versionable { + /** + * Returns a copy with the specified talent's value updated. + */ + fun withTalentRating(talentName: String, newValue: Int): Talents { + return copy( + talents = talents.map { talent -> + if (talent.name == talentName) { + Talent( + name = talent.name, + attribute = talent.attribute, + value = newValue, + custom = talent.custom + ) + } else { + talent + } + } + ) + } +}