feat: add editable talent ratings (Closes #25) #64
@@ -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"
|
||||
|
||||
@@ -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<AttributeType?>(null) }
|
||||
|
||||
// Talent edit dialog state
|
||||
var editingTalent by remember { mutableStateOf<TalentDefinition?>(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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -7,4 +7,24 @@ import org.shahondin1624.model.Versionable
|
||||
data class Talents(
|
||||
val talents: List<TalentDefinition> = 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
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user