This commit was merged in pull request #136.
This commit is contained in:
@@ -137,6 +137,38 @@
|
|||||||
<string name="talent_min_rating_error">Minimum rating is 0</string>
|
<string name="talent_min_rating_error">Minimum rating is 0</string>
|
||||||
<string name="talent_max_rating_error">Maximum rating is %1$d</string>
|
<string name="talent_max_rating_error">Maximum rating is %1$d</string>
|
||||||
|
|
||||||
|
<!-- Skill categories -->
|
||||||
|
<string name="skill_category_active">Active Skills</string>
|
||||||
|
<string name="skill_category_knowledge">Knowledge Skills</string>
|
||||||
|
<string name="skill_category_language">Language Skills</string>
|
||||||
|
<string name="skill_specialization_label">Specialization (optional)</string>
|
||||||
|
<string name="skill_specialization_placeholder">e.g., Pistols</string>
|
||||||
|
<string name="skill_roll_with_spec">Roll with specialization (+2)</string>
|
||||||
|
<string name="skill_roll_without_spec">Roll without specialization</string>
|
||||||
|
<string name="skill_untrained_default">Untrained (default)</string>
|
||||||
|
<string name="skill_add_knowledge">Add Knowledge Skill</string>
|
||||||
|
<string name="skill_add_language">Add Language Skill</string>
|
||||||
|
<string name="skill_knowledge_category_label">Category</string>
|
||||||
|
<string name="skill_language_proficiency_label">Proficiency</string>
|
||||||
|
<string name="skill_add_knowledge_title">Add Knowledge Skill</string>
|
||||||
|
<string name="skill_add_language_title">Add Language Skill</string>
|
||||||
|
<string name="skill_name_label">Skill Name</string>
|
||||||
|
<string name="skill_name_placeholder">e.g., Corporate Politics</string>
|
||||||
|
<string name="skill_language_name_placeholder">e.g., Japanese</string>
|
||||||
|
<string name="skill_remove_content_desc">Remove skill</string>
|
||||||
|
|
||||||
|
<!-- Knowledge categories -->
|
||||||
|
<string name="knowledge_academic">Academic</string>
|
||||||
|
<string name="knowledge_interest">Interest</string>
|
||||||
|
<string name="knowledge_professional">Professional</string>
|
||||||
|
<string name="knowledge_street">Street</string>
|
||||||
|
|
||||||
|
<!-- Language proficiencies -->
|
||||||
|
<string name="language_basic">Basic</string>
|
||||||
|
<string name="language_specialist">Specialist</string>
|
||||||
|
<string name="language_expert">Expert</string>
|
||||||
|
<string name="language_native">Native</string>
|
||||||
|
|
||||||
<!-- Dice roll content descriptions -->
|
<!-- Dice roll content descriptions -->
|
||||||
<string name="roll_dice_content_desc">Roll dice</string>
|
<string name="roll_dice_content_desc">Roll dice</string>
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,24 @@ object TestTags {
|
|||||||
const val TALENT_EDIT_CONFIRM = "talent_edit_confirm"
|
const val TALENT_EDIT_CONFIRM = "talent_edit_confirm"
|
||||||
const val TALENT_EDIT_DISMISS = "talent_edit_dismiss"
|
const val TALENT_EDIT_DISMISS = "talent_edit_dismiss"
|
||||||
const val TALENT_EDIT_ERROR = "talent_edit_error"
|
const val TALENT_EDIT_ERROR = "talent_edit_error"
|
||||||
|
const val TALENT_EDIT_SPECIALIZATION = "talent_edit_specialization"
|
||||||
|
|
||||||
|
// --- Knowledge/Language skill dialogs ---
|
||||||
|
const val KNOWLEDGE_SKILL_ADD_DIALOG = "knowledge_skill_add_dialog"
|
||||||
|
const val KNOWLEDGE_SKILL_NAME_INPUT = "knowledge_skill_name_input"
|
||||||
|
const val KNOWLEDGE_SKILL_CATEGORY = "knowledge_skill_category"
|
||||||
|
const val KNOWLEDGE_SKILL_CONFIRM = "knowledge_skill_confirm"
|
||||||
|
const val KNOWLEDGE_SKILL_DISMISS = "knowledge_skill_dismiss"
|
||||||
|
const val KNOWLEDGE_SKILL_ADD_BUTTON = "knowledge_skill_add_button"
|
||||||
|
|
||||||
|
const val LANGUAGE_SKILL_ADD_DIALOG = "language_skill_add_dialog"
|
||||||
|
const val LANGUAGE_SKILL_NAME_INPUT = "language_skill_name_input"
|
||||||
|
const val LANGUAGE_SKILL_PROFICIENCY = "language_skill_proficiency"
|
||||||
|
const val LANGUAGE_SKILL_CONFIRM = "language_skill_confirm"
|
||||||
|
const val LANGUAGE_SKILL_DISMISS = "language_skill_dismiss"
|
||||||
|
const val LANGUAGE_SKILL_ADD_BUTTON = "language_skill_add_button"
|
||||||
|
|
||||||
|
fun skillRemoveButton(name: String): String = "skill_remove_${name.lowercase().replace(" ", "_")}"
|
||||||
|
|
||||||
// --- Dice roll result dialog ---
|
// --- Dice roll result dialog ---
|
||||||
const val DICE_ROLL_DIALOG = "dice_roll_dialog"
|
const val DICE_ROLL_DIALOG = "dice_roll_dialog"
|
||||||
|
|||||||
+152
-8
@@ -7,14 +7,20 @@ import androidx.compose.material3.*
|
|||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import org.shahondin1624.lib.components.TestTags
|
||||||
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.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.lib.components.charactermodel.attributespage.TalentEditDialog
|
||||||
import org.shahondin1624.model.talents.TalentDefinition
|
import org.shahondin1624.model.talents.TalentDefinition
|
||||||
|
import org.shahondin1624.model.talents.createKnowledgeSkill
|
||||||
|
import org.shahondin1624.model.talents.createLanguageSkill
|
||||||
|
import org.shahondin1624.lib.components.charactermodel.attributespage.KnowledgeSkillDialog
|
||||||
|
import org.shahondin1624.lib.components.charactermodel.attributespage.LanguageSkillDialog
|
||||||
import org.shahondin1624.lib.functions.DiceRoll
|
import org.shahondin1624.lib.functions.DiceRoll
|
||||||
import org.shahondin1624.lib.functions.performExtendedTest
|
import org.shahondin1624.lib.functions.performExtendedTest
|
||||||
import org.shahondin1624.lib.functions.performOpposedTest
|
import org.shahondin1624.lib.functions.performOpposedTest
|
||||||
@@ -200,9 +206,11 @@ fun CharacterSheetPage(
|
|||||||
editingTalent?.let { talent ->
|
editingTalent?.let { talent ->
|
||||||
TalentEditDialog(
|
TalentEditDialog(
|
||||||
talent = talent,
|
talent = talent,
|
||||||
onConfirm = { newValue ->
|
onConfirm = { newValue, specialization ->
|
||||||
onUpdateCharacter { char ->
|
onUpdateCharacter { char ->
|
||||||
val newTalents = char.talents.withTalentRating(talent.name, newValue)
|
val newTalents = char.talents.withTalentRatingAndSpecialization(
|
||||||
|
talent.name, newValue, specialization
|
||||||
|
)
|
||||||
char.copy(talents = newTalents)
|
char.copy(talents = newTalents)
|
||||||
}
|
}
|
||||||
editingTalent = null
|
editingTalent = null
|
||||||
@@ -257,7 +265,7 @@ fun CharacterSheetPage(
|
|||||||
when (selectedTab) {
|
when (selectedTab) {
|
||||||
CharacterTab.Overview -> ExpandedOverviewContent(character, spacing, onEditCharacterData, onUpdateCharacter)
|
CharacterTab.Overview -> ExpandedOverviewContent(character, spacing, onEditCharacterData, onUpdateCharacter)
|
||||||
CharacterTab.Attributes -> AttributesContent(character, spacing, onDiceRoll, onEditAttribute)
|
CharacterTab.Attributes -> AttributesContent(character, spacing, onDiceRoll, onEditAttribute)
|
||||||
CharacterTab.Talents -> TalentsContent(character, spacing, onDiceRoll, onEditTalent)
|
CharacterTab.Talents -> TalentsContent(character, spacing, onDiceRoll, onEditTalent, onUpdateCharacter)
|
||||||
CharacterTab.Magic -> MagicContent(character, spacing, onUpdateCharacter)
|
CharacterTab.Magic -> MagicContent(character, spacing, onUpdateCharacter)
|
||||||
CharacterTab.Combat -> CombatContent(character, spacing, onUpdateCharacter)
|
CharacterTab.Combat -> CombatContent(character, spacing, onUpdateCharacter)
|
||||||
CharacterTab.Gear -> GearContent(character, spacing, onUpdateCharacter)
|
CharacterTab.Gear -> GearContent(character, spacing, onUpdateCharacter)
|
||||||
@@ -268,7 +276,7 @@ fun CharacterSheetPage(
|
|||||||
when (selectedTab) {
|
when (selectedTab) {
|
||||||
CharacterTab.Overview -> OverviewContent(character, spacing, onEditCharacterData, onUpdateCharacter)
|
CharacterTab.Overview -> OverviewContent(character, spacing, onEditCharacterData, onUpdateCharacter)
|
||||||
CharacterTab.Attributes -> AttributesContent(character, spacing, onDiceRoll, onEditAttribute)
|
CharacterTab.Attributes -> AttributesContent(character, spacing, onDiceRoll, onEditAttribute)
|
||||||
CharacterTab.Talents -> TalentsContent(character, spacing, onDiceRoll, onEditTalent)
|
CharacterTab.Talents -> TalentsContent(character, spacing, onDiceRoll, onEditTalent, onUpdateCharacter)
|
||||||
CharacterTab.Magic -> MagicContent(character, spacing, onUpdateCharacter)
|
CharacterTab.Magic -> MagicContent(character, spacing, onUpdateCharacter)
|
||||||
CharacterTab.Combat -> CombatContent(character, spacing, onUpdateCharacter)
|
CharacterTab.Combat -> CombatContent(character, spacing, onUpdateCharacter)
|
||||||
CharacterTab.Gear -> GearContent(character, spacing, onUpdateCharacter)
|
CharacterTab.Gear -> GearContent(character, spacing, onUpdateCharacter)
|
||||||
@@ -424,13 +432,43 @@ private fun TalentsContent(
|
|||||||
character: ShadowrunCharacter,
|
character: ShadowrunCharacter,
|
||||||
spacing: Dp,
|
spacing: Dp,
|
||||||
onDiceRoll: (DiceRoll, String) -> Unit,
|
onDiceRoll: (DiceRoll, String) -> Unit,
|
||||||
onEditTalent: (TalentDefinition) -> Unit
|
onEditTalent: (TalentDefinition) -> Unit,
|
||||||
|
onUpdateCharacter: ((ShadowrunCharacter) -> ShadowrunCharacter) -> 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)
|
||||||
val talentsPerRow = totalCols / talentSpan
|
val talentsPerRow = totalCols / talentSpan
|
||||||
|
|
||||||
|
var showAddKnowledgeDialog by remember { mutableStateOf(false) }
|
||||||
|
var showAddLanguageDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
if (showAddKnowledgeDialog) {
|
||||||
|
KnowledgeSkillDialog(
|
||||||
|
onConfirm = { name, category ->
|
||||||
|
onUpdateCharacter { char ->
|
||||||
|
val newTalent = createKnowledgeSkill(name, category)
|
||||||
|
char.copy(talents = char.talents.withAddedTalent(newTalent))
|
||||||
|
}
|
||||||
|
showAddKnowledgeDialog = false
|
||||||
|
},
|
||||||
|
onDismiss = { showAddKnowledgeDialog = false }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showAddLanguageDialog) {
|
||||||
|
LanguageSkillDialog(
|
||||||
|
onConfirm = { name, proficiency ->
|
||||||
|
onUpdateCharacter { char ->
|
||||||
|
val newTalent = createLanguageSkill(name, proficiency)
|
||||||
|
char.copy(talents = char.talents.withAddedTalent(newTalent))
|
||||||
|
}
|
||||||
|
showAddLanguageDialog = false
|
||||||
|
},
|
||||||
|
onDismiss = { showAddLanguageDialog = false }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -438,9 +476,15 @@ private fun TalentsContent(
|
|||||||
.padding(spacing),
|
.padding(spacing),
|
||||||
verticalArrangement = Arrangement.spacedBy(spacing)
|
verticalArrangement = Arrangement.spacedBy(spacing)
|
||||||
) {
|
) {
|
||||||
val talents = character.talents.talents
|
// Active Skills section
|
||||||
val rows = talents.chunked(talentsPerRow)
|
Text(
|
||||||
for (row in rows) {
|
text = stringResource(Res.string.skill_category_active),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
modifier = Modifier.padding(bottom = 4.dp)
|
||||||
|
)
|
||||||
|
val activeSkills = character.talents.activeSkills()
|
||||||
|
val activeRows = activeSkills.chunked(talentsPerRow)
|
||||||
|
for (row in activeRows) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(spacing)
|
horizontalArrangement = Arrangement.spacedBy(spacing)
|
||||||
@@ -460,6 +504,106 @@ private fun TalentsContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
HorizontalDivider(modifier = Modifier.padding(vertical = spacing))
|
||||||
|
|
||||||
|
// Knowledge Skills section
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.skill_category_knowledge),
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
TextButton(
|
||||||
|
onClick = { showAddKnowledgeDialog = true },
|
||||||
|
modifier = Modifier.testTag(TestTags.KNOWLEDGE_SKILL_ADD_BUTTON)
|
||||||
|
) {
|
||||||
|
Text(stringResource(Res.string.skill_add_knowledge))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val knowledgeSkills = character.talents.knowledgeSkills()
|
||||||
|
if (knowledgeSkills.isEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = "No knowledge skills",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val knowledgeRows = knowledgeSkills.chunked(talentsPerRow)
|
||||||
|
for (row in knowledgeRows) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(spacing)
|
||||||
|
) {
|
||||||
|
for (talent in row) {
|
||||||
|
Box(modifier = Modifier.weight(1f)) {
|
||||||
|
Talent(
|
||||||
|
talent = talent,
|
||||||
|
attributes = character.attributes,
|
||||||
|
onRoll = { roll -> onDiceRoll(roll, talent.name) },
|
||||||
|
onEdit = { onEditTalent(talent) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
repeat(talentsPerRow - row.size) {
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider(modifier = Modifier.padding(vertical = spacing))
|
||||||
|
|
||||||
|
// Language Skills section
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.skill_category_language),
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
TextButton(
|
||||||
|
onClick = { showAddLanguageDialog = true },
|
||||||
|
modifier = Modifier.testTag(TestTags.LANGUAGE_SKILL_ADD_BUTTON)
|
||||||
|
) {
|
||||||
|
Text(stringResource(Res.string.skill_add_language))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val languageSkills = character.talents.languageSkills()
|
||||||
|
if (languageSkills.isEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = "No language skills",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val languageRows = languageSkills.chunked(talentsPerRow)
|
||||||
|
for (row in languageRows) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(spacing)
|
||||||
|
) {
|
||||||
|
for (talent in row) {
|
||||||
|
Box(modifier = Modifier.weight(1f)) {
|
||||||
|
Talent(
|
||||||
|
talent = talent,
|
||||||
|
attributes = character.attributes,
|
||||||
|
onRoll = { roll -> onDiceRoll(roll, talent.name) },
|
||||||
|
onEdit = { onEditTalent(talent) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
repeat(talentsPerRow - row.size) {
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+109
@@ -0,0 +1,109 @@
|
|||||||
|
package org.shahondin1624.lib.components.charactermodel.attributespage
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import org.jetbrains.compose.resources.stringResource
|
||||||
|
import org.shahondin1624.lib.components.TestTags
|
||||||
|
import org.shahondin1624.model.talents.KnowledgeCategory
|
||||||
|
import shadowruncharsheet.sharedui.generated.resources.Res
|
||||||
|
import shadowruncharsheet.sharedui.generated.resources.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dialog for adding a new knowledge skill.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun KnowledgeSkillDialog(
|
||||||
|
onConfirm: (name: String, category: KnowledgeCategory) -> Unit,
|
||||||
|
onDismiss: () -> Unit
|
||||||
|
) {
|
||||||
|
var name by remember { mutableStateOf("") }
|
||||||
|
var selectedCategory by remember { mutableStateOf(KnowledgeCategory.ACADEMIC) }
|
||||||
|
var categoryExpanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val isValid = name.isNotBlank()
|
||||||
|
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
modifier = Modifier.testTag(TestTags.KNOWLEDGE_SKILL_ADD_DIALOG),
|
||||||
|
title = {
|
||||||
|
Text(text = stringResource(Res.string.skill_add_knowledge_title))
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = name,
|
||||||
|
onValueChange = { name = it },
|
||||||
|
label = { Text(stringResource(Res.string.skill_name_label)) },
|
||||||
|
placeholder = { Text(stringResource(Res.string.skill_name_placeholder)) },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.testTag(TestTags.KNOWLEDGE_SKILL_NAME_INPUT)
|
||||||
|
)
|
||||||
|
|
||||||
|
ExposedDropdownMenuBox(
|
||||||
|
expanded = categoryExpanded,
|
||||||
|
onExpandedChange = { categoryExpanded = it },
|
||||||
|
modifier = Modifier.testTag(TestTags.KNOWLEDGE_SKILL_CATEGORY)
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = knowledgeCategoryLabel(selectedCategory),
|
||||||
|
onValueChange = {},
|
||||||
|
readOnly = true,
|
||||||
|
label = { Text(stringResource(Res.string.skill_knowledge_category_label)) },
|
||||||
|
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = categoryExpanded) },
|
||||||
|
modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable).fillMaxWidth()
|
||||||
|
)
|
||||||
|
ExposedDropdownMenu(
|
||||||
|
expanded = categoryExpanded,
|
||||||
|
onDismissRequest = { categoryExpanded = false }
|
||||||
|
) {
|
||||||
|
KnowledgeCategory.entries.forEach { cat ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(knowledgeCategoryLabel(cat)) },
|
||||||
|
onClick = {
|
||||||
|
selectedCategory = cat
|
||||||
|
categoryExpanded = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = { if (isValid) onConfirm(name.trim(), selectedCategory) },
|
||||||
|
enabled = isValid,
|
||||||
|
modifier = Modifier.testTag(TestTags.KNOWLEDGE_SKILL_CONFIRM)
|
||||||
|
) {
|
||||||
|
Text(stringResource(Res.string.create))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = onDismiss,
|
||||||
|
modifier = Modifier.testTag(TestTags.KNOWLEDGE_SKILL_DISMISS)
|
||||||
|
) {
|
||||||
|
Text(stringResource(Res.string.cancel))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun knowledgeCategoryLabel(category: KnowledgeCategory): String {
|
||||||
|
return when (category) {
|
||||||
|
KnowledgeCategory.ACADEMIC -> stringResource(Res.string.knowledge_academic)
|
||||||
|
KnowledgeCategory.INTEREST -> stringResource(Res.string.knowledge_interest)
|
||||||
|
KnowledgeCategory.PROFESSIONAL -> stringResource(Res.string.knowledge_professional)
|
||||||
|
KnowledgeCategory.STREET -> stringResource(Res.string.knowledge_street)
|
||||||
|
}
|
||||||
|
}
|
||||||
+109
@@ -0,0 +1,109 @@
|
|||||||
|
package org.shahondin1624.lib.components.charactermodel.attributespage
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import org.jetbrains.compose.resources.stringResource
|
||||||
|
import org.shahondin1624.lib.components.TestTags
|
||||||
|
import org.shahondin1624.model.talents.LanguageProficiency
|
||||||
|
import shadowruncharsheet.sharedui.generated.resources.Res
|
||||||
|
import shadowruncharsheet.sharedui.generated.resources.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dialog for adding a new language skill.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun LanguageSkillDialog(
|
||||||
|
onConfirm: (name: String, proficiency: LanguageProficiency) -> Unit,
|
||||||
|
onDismiss: () -> Unit
|
||||||
|
) {
|
||||||
|
var name by remember { mutableStateOf("") }
|
||||||
|
var selectedProficiency by remember { mutableStateOf(LanguageProficiency.BASIC) }
|
||||||
|
var proficiencyExpanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val isValid = name.isNotBlank()
|
||||||
|
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
modifier = Modifier.testTag(TestTags.LANGUAGE_SKILL_ADD_DIALOG),
|
||||||
|
title = {
|
||||||
|
Text(text = stringResource(Res.string.skill_add_language_title))
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = name,
|
||||||
|
onValueChange = { name = it },
|
||||||
|
label = { Text(stringResource(Res.string.skill_name_label)) },
|
||||||
|
placeholder = { Text(stringResource(Res.string.skill_language_name_placeholder)) },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.testTag(TestTags.LANGUAGE_SKILL_NAME_INPUT)
|
||||||
|
)
|
||||||
|
|
||||||
|
ExposedDropdownMenuBox(
|
||||||
|
expanded = proficiencyExpanded,
|
||||||
|
onExpandedChange = { proficiencyExpanded = it },
|
||||||
|
modifier = Modifier.testTag(TestTags.LANGUAGE_SKILL_PROFICIENCY)
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = languageProficiencyLabel(selectedProficiency),
|
||||||
|
onValueChange = {},
|
||||||
|
readOnly = true,
|
||||||
|
label = { Text(stringResource(Res.string.skill_language_proficiency_label)) },
|
||||||
|
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = proficiencyExpanded) },
|
||||||
|
modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable).fillMaxWidth()
|
||||||
|
)
|
||||||
|
ExposedDropdownMenu(
|
||||||
|
expanded = proficiencyExpanded,
|
||||||
|
onDismissRequest = { proficiencyExpanded = false }
|
||||||
|
) {
|
||||||
|
LanguageProficiency.entries.forEach { prof ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(languageProficiencyLabel(prof)) },
|
||||||
|
onClick = {
|
||||||
|
selectedProficiency = prof
|
||||||
|
proficiencyExpanded = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = { if (isValid) onConfirm(name.trim(), selectedProficiency) },
|
||||||
|
enabled = isValid,
|
||||||
|
modifier = Modifier.testTag(TestTags.LANGUAGE_SKILL_CONFIRM)
|
||||||
|
) {
|
||||||
|
Text(stringResource(Res.string.create))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = onDismiss,
|
||||||
|
modifier = Modifier.testTag(TestTags.LANGUAGE_SKILL_DISMISS)
|
||||||
|
) {
|
||||||
|
Text(stringResource(Res.string.cancel))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun languageProficiencyLabel(proficiency: LanguageProficiency): String {
|
||||||
|
return when (proficiency) {
|
||||||
|
LanguageProficiency.BASIC -> stringResource(Res.string.language_basic)
|
||||||
|
LanguageProficiency.SPECIALIST -> stringResource(Res.string.language_specialist)
|
||||||
|
LanguageProficiency.EXPERT -> stringResource(Res.string.language_expert)
|
||||||
|
LanguageProficiency.NATIVE -> stringResource(Res.string.language_native)
|
||||||
|
}
|
||||||
|
}
|
||||||
+55
-4
@@ -11,10 +11,16 @@ import androidx.compose.foundation.layout.widthIn
|
|||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.DropdownMenu
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
@@ -30,6 +36,7 @@ import org.shahondin1624.lib.components.TestTags
|
|||||||
import org.shahondin1624.lib.components.UiConstants.SMALL_PADDING
|
import org.shahondin1624.lib.components.UiConstants.SMALL_PADDING
|
||||||
import org.shahondin1624.lib.functions.DiceRoll
|
import org.shahondin1624.lib.functions.DiceRoll
|
||||||
import org.shahondin1624.model.attributes.Attributes
|
import org.shahondin1624.model.attributes.Attributes
|
||||||
|
import org.shahondin1624.model.talents.Talent as TalentModel
|
||||||
import org.shahondin1624.model.talents.TalentDefinition
|
import org.shahondin1624.model.talents.TalentDefinition
|
||||||
import org.shahondin1624.theme.contrastTextColor
|
import org.shahondin1624.theme.contrastTextColor
|
||||||
import shadowruncharsheet.sharedui.generated.resources.Res
|
import shadowruncharsheet.sharedui.generated.resources.Res
|
||||||
@@ -76,6 +83,9 @@ private fun TalentCardContent(
|
|||||||
textColor: Color,
|
textColor: Color,
|
||||||
onRoll: (DiceRoll) -> Unit
|
onRoll: (DiceRoll) -> Unit
|
||||||
) {
|
) {
|
||||||
|
var showSpecMenu by remember { mutableStateOf(false) }
|
||||||
|
val hasSpec = talent.specialization != null
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.Start,
|
horizontalArrangement = Arrangement.Start,
|
||||||
@@ -94,7 +104,7 @@ private fun TalentCardContent(
|
|||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = talent.name,
|
text = if (talent is TalentModel) talent.displayName() else talent.name,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.padding(start = SMALL_PADDING),
|
.padding(start = SMALL_PADDING),
|
||||||
@@ -114,7 +124,11 @@ private fun TalentCardContent(
|
|||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = talent.value.toString(),
|
text = if (talent.value == 0) {
|
||||||
|
stringResource(Res.string.skill_untrained_default)
|
||||||
|
} else {
|
||||||
|
talent.value.toString()
|
||||||
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.widthIn(min = 24.dp)
|
.widthIn(min = 24.dp)
|
||||||
.padding(end = SMALL_PADDING),
|
.padding(end = SMALL_PADDING),
|
||||||
@@ -126,8 +140,16 @@ private fun TalentCardContent(
|
|||||||
}
|
}
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
val result = talent.test(modifiers = emptyList(), attributes = attributes)
|
if (hasSpec) {
|
||||||
onRoll(result)
|
showSpecMenu = true
|
||||||
|
} else {
|
||||||
|
val result = talent.test(
|
||||||
|
modifiers = emptyList(),
|
||||||
|
attributes = attributes,
|
||||||
|
withSpecialization = false
|
||||||
|
)
|
||||||
|
onRoll(result)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(end = SMALL_PADDING)
|
.padding(end = SMALL_PADDING)
|
||||||
@@ -141,6 +163,35 @@ private fun TalentCardContent(
|
|||||||
),
|
),
|
||||||
modifier = Modifier.size(24.dp)
|
modifier = Modifier.size(24.dp)
|
||||||
)
|
)
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = showSpecMenu,
|
||||||
|
onDismissRequest = { showSpecMenu = false }
|
||||||
|
) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(stringResource(Res.string.skill_roll_without_spec)) },
|
||||||
|
onClick = {
|
||||||
|
showSpecMenu = false
|
||||||
|
val result = talent.test(
|
||||||
|
modifiers = emptyList(),
|
||||||
|
attributes = attributes,
|
||||||
|
withSpecialization = false
|
||||||
|
)
|
||||||
|
onRoll(result)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(stringResource(Res.string.skill_roll_with_spec)) },
|
||||||
|
onClick = {
|
||||||
|
showSpecMenu = false
|
||||||
|
val result = talent.test(
|
||||||
|
modifiers = emptyList(),
|
||||||
|
attributes = attributes,
|
||||||
|
withSpecialization = true
|
||||||
|
)
|
||||||
|
onRoll(result)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-3
@@ -15,17 +15,18 @@ import shadowruncharsheet.sharedui.generated.resources.Res
|
|||||||
import shadowruncharsheet.sharedui.generated.resources.*
|
import shadowruncharsheet.sharedui.generated.resources.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dialog for editing a talent's rating.
|
* Dialog for editing a talent's rating and optional specialization.
|
||||||
* Rating range is 0-12; 0 means untrained.
|
* Rating range is 0-12; 0 means untrained.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun TalentEditDialog(
|
fun TalentEditDialog(
|
||||||
talent: TalentDefinition,
|
talent: TalentDefinition,
|
||||||
maxValue: Int = 12,
|
maxValue: Int = 12,
|
||||||
onConfirm: (Int) -> Unit,
|
onConfirm: (Int, String?) -> Unit,
|
||||||
onDismiss: () -> Unit
|
onDismiss: () -> Unit
|
||||||
) {
|
) {
|
||||||
var textValue by remember { mutableStateOf(talent.value.toString()) }
|
var textValue by remember { mutableStateOf(talent.value.toString()) }
|
||||||
|
var specValue by remember { mutableStateOf(talent.specialization ?: "") }
|
||||||
val parsedValue = textValue.toIntOrNull()
|
val parsedValue = textValue.toIntOrNull()
|
||||||
val isValid = parsedValue != null && parsedValue in 0..maxValue
|
val isValid = parsedValue != null && parsedValue in 0..maxValue
|
||||||
|
|
||||||
@@ -68,11 +69,27 @@ fun TalentEditDialog(
|
|||||||
modifier = Modifier.testTag(TestTags.TALENT_EDIT_ERROR)
|
modifier = Modifier.testTag(TestTags.TALENT_EDIT_ERROR)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = specValue,
|
||||||
|
onValueChange = { specValue = it },
|
||||||
|
label = { Text(stringResource(Res.string.skill_specialization_label)) },
|
||||||
|
placeholder = { Text(stringResource(Res.string.skill_specialization_placeholder)) },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.testTag(TestTags.TALENT_EDIT_SPECIALIZATION)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = { if (isValid) onConfirm(parsedValue!!) },
|
onClick = {
|
||||||
|
if (isValid) {
|
||||||
|
val spec = specValue.takeIf { it.isNotBlank() }
|
||||||
|
onConfirm(parsedValue!!, spec)
|
||||||
|
}
|
||||||
|
},
|
||||||
enabled = isValid,
|
enabled = isValid,
|
||||||
modifier = Modifier.testTag(TestTags.TALENT_EDIT_CONFIRM)
|
modifier = Modifier.testTag(TestTags.TALENT_EDIT_CONFIRM)
|
||||||
) {
|
) {
|
||||||
|
|||||||
+2
-1
@@ -13,7 +13,8 @@ object MigrationRegistry {
|
|||||||
private val migrations: List<VersionMigration> = listOf(
|
private val migrations: List<VersionMigration> = listOf(
|
||||||
MigrationV01ToV02(),
|
MigrationV01ToV02(),
|
||||||
MigrationV02ToV03(),
|
MigrationV02ToV03(),
|
||||||
MigrationV03ToV04()
|
MigrationV03ToV04(),
|
||||||
|
MigrationV04ToV05()
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package org.shahondin1624.model.migration
|
||||||
|
|
||||||
|
import kotlinx.serialization.json.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migration from schema v0.4 to v0.5.
|
||||||
|
*
|
||||||
|
* Changes in v0.5:
|
||||||
|
* - Adds "specialization" field (null) to all talent entries
|
||||||
|
* - Adds "category" field ("ACTIVE") to all talent entries
|
||||||
|
* - Adds "knowledgeCategory" field (null) to all talent entries
|
||||||
|
* - Adds "languageProficiency" field (null) to all talent entries
|
||||||
|
* - Updates all version fields from "v0.4" to "v0.5"
|
||||||
|
*/
|
||||||
|
class MigrationV04ToV05 : VersionMigration {
|
||||||
|
override val fromVersion: String = SchemaVersion.V0_4
|
||||||
|
override val toVersion: String = SchemaVersion.V0_5
|
||||||
|
|
||||||
|
override fun migrate(json: JsonObject): JsonObject {
|
||||||
|
val mutable = json.toMutableMap()
|
||||||
|
|
||||||
|
// Migrate talent entries to add new fields
|
||||||
|
val talents = mutable["talents"]
|
||||||
|
if (talents is JsonObject) {
|
||||||
|
val talentsMutable = talents.toMutableMap()
|
||||||
|
val talentList = talentsMutable["talents"]
|
||||||
|
if (talentList is JsonArray) {
|
||||||
|
val migratedTalents = talentList.map { talentElement ->
|
||||||
|
if (talentElement is JsonObject) {
|
||||||
|
migrateTalent(talentElement)
|
||||||
|
} else {
|
||||||
|
talentElement
|
||||||
|
}
|
||||||
|
}
|
||||||
|
talentsMutable["talents"] = JsonArray(migratedTalents)
|
||||||
|
}
|
||||||
|
talentsMutable["version"] = JsonPrimitive(toVersion)
|
||||||
|
mutable["talents"] = JsonObject(talentsMutable)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update version fields at all levels
|
||||||
|
mutable["version"] = JsonPrimitive(toVersion)
|
||||||
|
mutable.updateNestedVersion("characterData")
|
||||||
|
mutable.updateNestedVersion("attributes")
|
||||||
|
mutable.updateNestedVersion("damageMonitor")
|
||||||
|
|
||||||
|
return JsonObject(mutable)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun migrateTalent(talent: JsonObject): JsonObject {
|
||||||
|
val mutable = talent.toMutableMap()
|
||||||
|
|
||||||
|
if ("specialization" !in mutable) {
|
||||||
|
mutable["specialization"] = JsonNull
|
||||||
|
}
|
||||||
|
if ("category" !in mutable) {
|
||||||
|
mutable["category"] = JsonPrimitive("ACTIVE")
|
||||||
|
}
|
||||||
|
if ("knowledgeCategory" !in mutable) {
|
||||||
|
mutable["knowledgeCategory"] = JsonNull
|
||||||
|
}
|
||||||
|
if ("languageProficiency" !in mutable) {
|
||||||
|
mutable["languageProficiency"] = JsonNull
|
||||||
|
}
|
||||||
|
|
||||||
|
return JsonObject(mutable)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun MutableMap<String, JsonElement>.updateNestedVersion(key: String) {
|
||||||
|
val nested = this[key]
|
||||||
|
if (nested is JsonObject) {
|
||||||
|
val nestedMutable = nested.toMutableMap()
|
||||||
|
nestedMutable["version"] = JsonPrimitive(toVersion)
|
||||||
|
this[key] = JsonObject(nestedMutable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ package org.shahondin1624.model.migration
|
|||||||
* as their default version value.
|
* as their default version value.
|
||||||
*/
|
*/
|
||||||
object SchemaVersion {
|
object SchemaVersion {
|
||||||
const val CURRENT: String = "v0.4"
|
const val CURRENT: String = "v0.5"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initial version used before the migration system was introduced.
|
* Initial version used before the migration system was introduced.
|
||||||
@@ -17,4 +17,6 @@ object SchemaVersion {
|
|||||||
const val V0_3: String = "v0.3"
|
const val V0_3: String = "v0.3"
|
||||||
|
|
||||||
const val V0_4: String = "v0.4"
|
const val V0_4: String = "v0.4"
|
||||||
|
|
||||||
|
const val V0_5: String = "v0.5"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package org.shahondin1624.model.talents
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sub-categories for knowledge skills in Shadowrun 5e.
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
enum class KnowledgeCategory {
|
||||||
|
ACADEMIC,
|
||||||
|
INTEREST,
|
||||||
|
PROFESSIONAL,
|
||||||
|
STREET
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package org.shahondin1624.model.talents
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Proficiency levels for language skills in Shadowrun 5e.
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
enum class LanguageProficiency {
|
||||||
|
BASIC,
|
||||||
|
SPECIALIST,
|
||||||
|
EXPERT,
|
||||||
|
NATIVE
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package org.shahondin1624.model.talents
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Categorizes skills according to Shadowrun 5e rules.
|
||||||
|
* - ACTIVE: Combat, social, technical, vehicle, and magical skills
|
||||||
|
* - KNOWLEDGE: Academic, interest, professional, and street knowledge
|
||||||
|
* - LANGUAGE: Language skills with proficiency levels
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
enum class SkillCategory {
|
||||||
|
ACTIVE,
|
||||||
|
KNOWLEDGE,
|
||||||
|
LANGUAGE
|
||||||
|
}
|
||||||
@@ -15,11 +15,42 @@ data class Talent(
|
|||||||
override val attribute: AttributeType,
|
override val attribute: AttributeType,
|
||||||
override val value: Int,
|
override val value: Int,
|
||||||
override val custom: Boolean,
|
override val custom: Boolean,
|
||||||
|
override val specialization: String? = null,
|
||||||
|
override val category: SkillCategory = SkillCategory.ACTIVE,
|
||||||
|
override val knowledgeCategory: KnowledgeCategory? = null,
|
||||||
|
override val languageProficiency: LanguageProficiency? = null,
|
||||||
): TalentDefinition {
|
): TalentDefinition {
|
||||||
override fun test(modifiers: List<SRModifier<*>>, attributes: Attributes): DiceRoll {
|
override fun test(
|
||||||
|
modifiers: List<SRModifier<*>>,
|
||||||
|
attributes: Attributes,
|
||||||
|
withSpecialization: Boolean
|
||||||
|
): DiceRoll {
|
||||||
val attributeValue = attributes.getAttributeByType(attribute).value
|
val attributeValue = attributes.getAttributeByType(attribute).value
|
||||||
val combined = value + attributeValue
|
val specBonus = if (withSpecialization && specialization != null) 2 else 0
|
||||||
return modifiers.accumulateModifiers(DiceRoll(numberOfDice = combined)).roll()
|
val pool = if (value == 0) {
|
||||||
|
defaultPool(attributes)
|
||||||
|
} else {
|
||||||
|
value + attributeValue + specBonus
|
||||||
|
}
|
||||||
|
return modifiers.accumulateModifiers(DiceRoll(numberOfDice = pool.coerceAtLeast(0))).roll()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun defaultPool(attributes: Attributes): Int {
|
||||||
|
return when (category) {
|
||||||
|
SkillCategory.ACTIVE -> {
|
||||||
|
(attributes.getAttributeByType(attribute).value - 1).coerceAtLeast(0)
|
||||||
|
}
|
||||||
|
SkillCategory.KNOWLEDGE, SkillCategory.LANGUAGE -> {
|
||||||
|
(attributes.getAttributeByType(AttributeType.Intuition).value - 1).coerceAtLeast(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display name including specialization, e.g., "Firearms (Pistols)"
|
||||||
|
*/
|
||||||
|
fun displayName(): String {
|
||||||
|
return if (specialization != null) "$name ($specialization)" else name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,11 +58,38 @@ private fun createProvidedTalent(provided: ProvidedTalentName, value: Int = 0) =
|
|||||||
name = provided.name.replace("_", " "),
|
name = provided.name.replace("_", " "),
|
||||||
attribute = provided.attribute,
|
attribute = provided.attribute,
|
||||||
value = value,
|
value = value,
|
||||||
custom = false
|
custom = false,
|
||||||
|
category = SkillCategory.ACTIVE
|
||||||
)
|
)
|
||||||
|
|
||||||
fun createAllProvidedTalents() = ProvidedTalentName.entries.map {
|
fun createAllProvidedTalents() = ProvidedTalentName.entries.map {
|
||||||
createProvidedTalent(it)
|
createProvidedTalent(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createCustomTalent(name: String, attribute: AttributeType, value: Int = 0) = Talent(name, attribute, value, true)
|
fun createCustomTalent(name: String, attribute: AttributeType, value: Int = 0) = Talent(name, attribute, value, true)
|
||||||
|
|
||||||
|
fun createKnowledgeSkill(
|
||||||
|
name: String,
|
||||||
|
knowledgeCategory: KnowledgeCategory,
|
||||||
|
value: Int = 0
|
||||||
|
) = Talent(
|
||||||
|
name = name,
|
||||||
|
attribute = AttributeType.Intuition,
|
||||||
|
value = value,
|
||||||
|
custom = true,
|
||||||
|
category = SkillCategory.KNOWLEDGE,
|
||||||
|
knowledgeCategory = knowledgeCategory
|
||||||
|
)
|
||||||
|
|
||||||
|
fun createLanguageSkill(
|
||||||
|
name: String,
|
||||||
|
proficiency: LanguageProficiency,
|
||||||
|
value: Int = 0
|
||||||
|
) = Talent(
|
||||||
|
name = name,
|
||||||
|
attribute = AttributeType.Intuition,
|
||||||
|
value = value,
|
||||||
|
custom = true,
|
||||||
|
category = SkillCategory.LANGUAGE,
|
||||||
|
languageProficiency = proficiency
|
||||||
|
)
|
||||||
|
|||||||
@@ -12,5 +12,25 @@ sealed interface TalentDefinition {
|
|||||||
val attribute: AttributeType
|
val attribute: AttributeType
|
||||||
val value: Int
|
val value: Int
|
||||||
val custom: Boolean
|
val custom: Boolean
|
||||||
fun test(modifiers: List<SRModifier<*>>, attributes: Attributes): DiceRoll
|
val specialization: String?
|
||||||
}
|
val category: SkillCategory
|
||||||
|
val knowledgeCategory: KnowledgeCategory?
|
||||||
|
val languageProficiency: LanguageProficiency?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Roll a skill test. When [withSpecialization] is true and a specialization
|
||||||
|
* is set, adds +2 dice to the pool per Shadowrun 5e rules.
|
||||||
|
*/
|
||||||
|
fun test(
|
||||||
|
modifiers: List<SRModifier<*>>,
|
||||||
|
attributes: Attributes,
|
||||||
|
withSpecialization: Boolean = false
|
||||||
|
): DiceRoll
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the default dice pool for an untrained test (skill rating 0).
|
||||||
|
* Active skills: linked attribute - 1 (minimum 0).
|
||||||
|
* Knowledge/Language skills: Intuition - 1 (minimum 0).
|
||||||
|
*/
|
||||||
|
fun defaultPool(attributes: Attributes): Int
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,11 +16,39 @@ data class Talents(
|
|||||||
return copy(
|
return copy(
|
||||||
talents = talents.map { talent ->
|
talents = talents.map { talent ->
|
||||||
if (talent.name == talentName) {
|
if (talent.name == talentName) {
|
||||||
Talent(
|
(talent as Talent).copy(value = newValue)
|
||||||
name = talent.name,
|
} else {
|
||||||
attribute = talent.attribute,
|
talent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a copy with the specified talent's specialization updated.
|
||||||
|
*/
|
||||||
|
fun withTalentSpecialization(talentName: String, specialization: String?): Talents {
|
||||||
|
return copy(
|
||||||
|
talents = talents.map { talent ->
|
||||||
|
if (talent.name == talentName) {
|
||||||
|
(talent as Talent).copy(specialization = specialization?.takeIf { it.isNotBlank() })
|
||||||
|
} else {
|
||||||
|
talent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a copy with the specified talent's rating and specialization updated together.
|
||||||
|
*/
|
||||||
|
fun withTalentRatingAndSpecialization(talentName: String, newValue: Int, specialization: String?): Talents {
|
||||||
|
return copy(
|
||||||
|
talents = talents.map { talent ->
|
||||||
|
if (talent.name == talentName) {
|
||||||
|
(talent as Talent).copy(
|
||||||
value = newValue,
|
value = newValue,
|
||||||
custom = talent.custom
|
specialization = specialization?.takeIf { it.isNotBlank() }
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
talent
|
talent
|
||||||
@@ -28,4 +56,27 @@ data class Talents(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new talent (knowledge or language skill) to the list.
|
||||||
|
*/
|
||||||
|
fun withAddedTalent(talent: TalentDefinition): Talents {
|
||||||
|
return copy(talents = talents + talent)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a talent by name. Only custom talents can be removed.
|
||||||
|
*/
|
||||||
|
fun withRemovedTalent(talentName: String): Talents {
|
||||||
|
return copy(talents = talents.filter { it.name != talentName || !it.custom })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** All active skills (the default Shadowrun skill list). */
|
||||||
|
fun activeSkills(): List<TalentDefinition> = talents.filter { it.category == SkillCategory.ACTIVE }
|
||||||
|
|
||||||
|
/** All knowledge skills. */
|
||||||
|
fun knowledgeSkills(): List<TalentDefinition> = talents.filter { it.category == SkillCategory.KNOWLEDGE }
|
||||||
|
|
||||||
|
/** All language skills. */
|
||||||
|
fun languageSkills(): List<TalentDefinition> = talents.filter { it.category == SkillCategory.LANGUAGE }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,13 +132,15 @@ class VersionMigrationTest {
|
|||||||
@Test
|
@Test
|
||||||
fun findMigrationChainReturnsPathFromV01() {
|
fun findMigrationChainReturnsPathFromV01() {
|
||||||
val chain = MigrationRegistry.findMigrationChain("v0.1")
|
val chain = MigrationRegistry.findMigrationChain("v0.1")
|
||||||
assertEquals(3, chain.size)
|
assertEquals(4, chain.size)
|
||||||
assertEquals("v0.1", chain[0].fromVersion)
|
assertEquals("v0.1", chain[0].fromVersion)
|
||||||
assertEquals("v0.2", chain[0].toVersion)
|
assertEquals("v0.2", chain[0].toVersion)
|
||||||
assertEquals("v0.2", chain[1].fromVersion)
|
assertEquals("v0.2", chain[1].fromVersion)
|
||||||
assertEquals("v0.3", chain[1].toVersion)
|
assertEquals("v0.3", chain[1].toVersion)
|
||||||
assertEquals("v0.3", chain[2].fromVersion)
|
assertEquals("v0.3", chain[2].fromVersion)
|
||||||
assertEquals("v0.4", chain[2].toVersion)
|
assertEquals("v0.4", chain[2].toVersion)
|
||||||
|
assertEquals("v0.4", chain[3].fromVersion)
|
||||||
|
assertEquals("v0.5", chain[3].toVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
Reference in New Issue
Block a user