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_DISMISS = "attribute_edit_dismiss"
|
||||||
const val ATTRIBUTE_EDIT_ERROR = "attribute_edit_error"
|
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 ---
|
// --- Dice roll result dialog ---
|
||||||
const val DICE_ROLL_DIALOG = "dice_roll_dialog"
|
const val DICE_ROLL_DIALOG = "dice_roll_dialog"
|
||||||
const val DICE_ROLL_DICE_COUNT = "dice_roll_dice_count"
|
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.Attribute
|
||||||
import org.shahondin1624.lib.components.charactermodel.attributespage.AttributeEditDialog
|
import org.shahondin1624.lib.components.charactermodel.attributespage.AttributeEditDialog
|
||||||
import org.shahondin1624.lib.components.charactermodel.attributespage.Talent
|
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.lib.functions.DiceRoll
|
||||||
import org.shahondin1624.model.attributes.AttributeType
|
import org.shahondin1624.model.attributes.AttributeType
|
||||||
import org.shahondin1624.model.charactermodel.ShadowrunCharacter
|
import org.shahondin1624.model.charactermodel.ShadowrunCharacter
|
||||||
@@ -54,6 +56,9 @@ fun CharacterSheetPage(
|
|||||||
// Attribute edit dialog state
|
// Attribute edit dialog state
|
||||||
var editingAttributeType by remember { mutableStateOf<AttributeType?>(null) }
|
var editingAttributeType by remember { mutableStateOf<AttributeType?>(null) }
|
||||||
|
|
||||||
|
// Talent edit dialog state
|
||||||
|
var editingTalent by remember { mutableStateOf<TalentDefinition?>(null) }
|
||||||
|
|
||||||
// Show dice roll result dialog
|
// Show dice roll result dialog
|
||||||
pendingRoll?.let { roll ->
|
pendingRoll?.let { roll ->
|
||||||
DiceRollResultDialog(
|
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()) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
// Tab row
|
// Tab row
|
||||||
TabRow(
|
TabRow(
|
||||||
@@ -97,6 +117,9 @@ fun CharacterSheetPage(
|
|||||||
val onEditAttribute: (AttributeType) -> Unit = { type ->
|
val onEditAttribute: (AttributeType) -> Unit = { type ->
|
||||||
editingAttributeType = type
|
editingAttributeType = type
|
||||||
}
|
}
|
||||||
|
val onEditTalent: (TalentDefinition) -> Unit = { talent ->
|
||||||
|
editingTalent = talent
|
||||||
|
}
|
||||||
|
|
||||||
when (windowSizeClass) {
|
when (windowSizeClass) {
|
||||||
WindowSizeClass.Expanded -> {
|
WindowSizeClass.Expanded -> {
|
||||||
@@ -104,7 +127,7 @@ fun CharacterSheetPage(
|
|||||||
when (selectedTab) {
|
when (selectedTab) {
|
||||||
CharacterTab.Overview -> ExpandedOverviewContent(character, spacing)
|
CharacterTab.Overview -> ExpandedOverviewContent(character, spacing)
|
||||||
CharacterTab.Attributes -> AttributesContent(character, spacing, onDiceRoll, onEditAttribute)
|
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)
|
CharacterTab.Combat -> CombatContent(character, spacing)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -112,7 +135,7 @@ fun CharacterSheetPage(
|
|||||||
when (selectedTab) {
|
when (selectedTab) {
|
||||||
CharacterTab.Overview -> OverviewContent(character, spacing)
|
CharacterTab.Overview -> OverviewContent(character, spacing)
|
||||||
CharacterTab.Attributes -> AttributesContent(character, spacing, onDiceRoll, onEditAttribute)
|
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)
|
CharacterTab.Combat -> CombatContent(character, spacing)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,7 +227,8 @@ private fun AttributesContent(
|
|||||||
private fun TalentsContent(
|
private fun TalentsContent(
|
||||||
character: ShadowrunCharacter,
|
character: ShadowrunCharacter,
|
||||||
spacing: Dp,
|
spacing: Dp,
|
||||||
onDiceRoll: (DiceRoll, String) -> Unit
|
onDiceRoll: (DiceRoll, String) -> Unit,
|
||||||
|
onEditTalent: (TalentDefinition) -> Unit
|
||||||
) {
|
) {
|
||||||
val windowSizeClass = LocalWindowSizeClass.current
|
val windowSizeClass = LocalWindowSizeClass.current
|
||||||
val totalCols = UiConstants.Grid.totalColumns(windowSizeClass)
|
val totalCols = UiConstants.Grid.totalColumns(windowSizeClass)
|
||||||
@@ -230,7 +254,8 @@ private fun TalentsContent(
|
|||||||
Talent(
|
Talent(
|
||||||
talent = talent,
|
talent = talent,
|
||||||
attributes = character.attributes,
|
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.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
@@ -42,10 +43,18 @@ fun Talent(
|
|||||||
onRoll: (DiceRoll) -> Unit = {
|
onRoll: (DiceRoll) -> Unit = {
|
||||||
println("Result for $talent: ${it.result}")
|
println("Result for $talent: ${it.result}")
|
||||||
},
|
},
|
||||||
|
onEdit: (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
var isInDarkMode by LocalThemeIsDark.current
|
var isInDarkMode by LocalThemeIsDark.current
|
||||||
val textColor = if (isInDarkMode) Color.Black else Color.White
|
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(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.Start,
|
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(
|
data class Talents(
|
||||||
val talents: List<TalentDefinition> = createAllProvidedTalents(),
|
val talents: List<TalentDefinition> = createAllProvidedTalents(),
|
||||||
override val version: String = "v0.1"
|
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