feat: add Create New Character action with confirmation dialog (Closes #30)

Add createNewCharacter() factory producing a blank ShadowrunCharacter
with all attributes at 1, all talents at rating 0, and default data.
Settings page gains a "New Character" button that shows a confirmation
prompt before replacing the current character. Includes 9 unit tests
verifying the blank character defaults.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
shahondin1624
2026-03-13 14:17:25 +01:00
parent 8f6d30dd69
commit bd37709663
4 changed files with 169 additions and 0 deletions

View File

@@ -77,6 +77,12 @@ object TestTags {
const val SETTINGS_THEME_DARK = "settings_theme_dark" const val SETTINGS_THEME_DARK = "settings_theme_dark"
const val SETTINGS_EXPORT_BUTTON = "settings_export_button" const val SETTINGS_EXPORT_BUTTON = "settings_export_button"
const val SETTINGS_IMPORT_BUTTON = "settings_import_button" const val SETTINGS_IMPORT_BUTTON = "settings_import_button"
const val SETTINGS_NEW_CHARACTER_BUTTON = "settings_new_character_button"
// --- New character confirmation dialog ---
const val NEW_CHARACTER_CONFIRM_DIALOG = "new_character_confirm_dialog"
const val NEW_CHARACTER_CONFIRM = "new_character_confirm"
const val NEW_CHARACTER_DISMISS = "new_character_dismiss"
// --- Export dialog --- // --- Export dialog ---
const val EXPORT_DIALOG = "export_dialog" const val EXPORT_DIALOG = "export_dialog"

View File

@@ -6,6 +6,7 @@ import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.selection.selectableGroup import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Download import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.Upload import androidx.compose.material.icons.filled.Upload
import androidx.compose.material3.* import androidx.compose.material3.*
@@ -19,6 +20,7 @@ import androidx.compose.ui.unit.dp
import org.shahondin1624.lib.components.TestTags import org.shahondin1624.lib.components.TestTags
import org.shahondin1624.lib.functions.DataLoader import org.shahondin1624.lib.functions.DataLoader
import org.shahondin1624.model.charactermodel.ShadowrunCharacter import org.shahondin1624.model.charactermodel.ShadowrunCharacter
import org.shahondin1624.model.createNewCharacter
import org.shahondin1624.theme.LocalThemePreference import org.shahondin1624.theme.LocalThemePreference
import org.shahondin1624.theme.ThemePreference import org.shahondin1624.theme.ThemePreference
@@ -33,6 +35,38 @@ fun SettingsPage(
var themePreference by LocalThemePreference.current var themePreference by LocalThemePreference.current
var showExportDialog by remember { mutableStateOf(false) } var showExportDialog by remember { mutableStateOf(false) }
var showImportDialog by remember { mutableStateOf(false) } var showImportDialog by remember { mutableStateOf(false) }
var showNewCharacterConfirm by remember { mutableStateOf(false) }
// New character confirmation dialog
if (showNewCharacterConfirm && onImportCharacter != null) {
AlertDialog(
onDismissRequest = { showNewCharacterConfirm = false },
modifier = Modifier.testTag(TestTags.NEW_CHARACTER_CONFIRM_DIALOG),
title = { Text("Create New Character") },
text = {
Text("This will replace your current character with a blank one. Any unsaved changes will be lost. Are you sure?")
},
confirmButton = {
TextButton(
onClick = {
onImportCharacter(createNewCharacter())
showNewCharacterConfirm = false
},
modifier = Modifier.testTag(TestTags.NEW_CHARACTER_CONFIRM)
) {
Text("Create")
}
},
dismissButton = {
TextButton(
onClick = { showNewCharacterConfirm = false },
modifier = Modifier.testTag(TestTags.NEW_CHARACTER_DISMISS)
) {
Text("Cancel")
}
}
)
}
// Export dialog // Export dialog
if (showExportDialog && character != null) { if (showExportDialog && character != null) {
@@ -163,6 +197,28 @@ fun SettingsPage(
Text("Import") Text("Import")
} }
} }
HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
OutlinedButton(
onClick = { showNewCharacterConfirm = true },
modifier = Modifier
.fillMaxWidth()
.testTag(TestTags.SETTINGS_NEW_CHARACTER_BUTTON)
) {
Icon(
Icons.Default.Add,
contentDescription = null,
modifier = Modifier.size(18.dp)
)
Spacer(Modifier.width(8.dp))
Text("New Character")
}
}
} }
} }
} }

View File

@@ -22,6 +22,42 @@ val EXAMPLE_ATTRIBUTES = Attributes(
edge = 2 edge = 2
) )
private val DEFAULT_ATTRIBUTES = Attributes(
body = Attribute(AttributeType.Body, 1),
agility = Attribute(AttributeType.Agility, 1),
reaction = Attribute(AttributeType.Reaction, 1),
strength = Attribute(AttributeType.Strength, 1),
willpower = Attribute(AttributeType.Willpower, 1),
logic = Attribute(AttributeType.Logic, 1),
intuition = Attribute(AttributeType.Intuition, 1),
charisma = Attribute(AttributeType.Charisma, 1),
edge = 1
)
/**
* Creates a fresh blank character with all attributes at 1, all talents at rating 0,
* and default character data. Used for the "New Character" action.
*/
fun createNewCharacter(): ShadowrunCharacter = ShadowrunCharacter(
attributes = DEFAULT_ATTRIBUTES,
talents = Talents(createAllProvidedTalents()),
characterData = CharacterData(
concept = "",
nuyen = 0,
essence = 6.0f,
name = "New Character",
metatype = Metatype.Human,
age = 0,
gender = "",
streetCred = 0,
notoriety = 0,
publicAwareness = 0,
totalKarma = 0,
currentKarma = 0
),
damageMonitor = createDamageMonitor(DEFAULT_ATTRIBUTES),
)
val EXAMPLE_CHARACTER: ShadowrunCharacter = ShadowrunCharacter( val EXAMPLE_CHARACTER: ShadowrunCharacter = ShadowrunCharacter(
attributes = EXAMPLE_ATTRIBUTES, attributes = EXAMPLE_ATTRIBUTES,
talents = Talents(createAllProvidedTalents()), talents = Talents(createAllProvidedTalents()),

View File

@@ -0,0 +1,71 @@
package org.shahondin1624
import org.shahondin1624.model.characterdata.Metatype
import org.shahondin1624.model.createNewCharacter
import kotlin.test.Test
import kotlin.test.assertEquals
class NewCharacterTest {
@Test
fun newCharacterHasDefaultName() {
val char = createNewCharacter()
assertEquals("New Character", char.characterData.name)
}
@Test
fun newCharacterHasAllAttributesAtOne() {
val char = createNewCharacter()
val attrs = char.attributes.getAllAttributes()
for (attr in attrs) {
assertEquals(1, attr.value, "Attribute ${attr.type.name} should be 1")
}
}
@Test
fun newCharacterHasEdgeOne() {
val char = createNewCharacter()
assertEquals(1, char.attributes.edge)
}
@Test
fun newCharacterHasAllTalentsAtZero() {
val char = createNewCharacter()
for (talent in char.talents.talents) {
assertEquals(0, talent.value, "Talent ${talent.name} should be 0")
}
}
@Test
fun newCharacterHasZeroKarma() {
val char = createNewCharacter()
assertEquals(0, char.characterData.totalKarma)
assertEquals(0, char.characterData.currentKarma)
}
@Test
fun newCharacterHasZeroNuyen() {
val char = createNewCharacter()
assertEquals(0, char.characterData.nuyen)
}
@Test
fun newCharacterHasFullEssence() {
val char = createNewCharacter()
assertEquals(6.0f, char.characterData.essence)
}
@Test
fun newCharacterIsHumanByDefault() {
val char = createNewCharacter()
assertEquals(Metatype.Human, char.characterData.metatype)
}
@Test
fun newCharacterHasZeroReputation() {
val char = createNewCharacter()
assertEquals(0, char.characterData.streetCred)
assertEquals(0, char.characterData.notoriety)
assertEquals(0, char.characterData.publicAwareness)
}
}