Refactor the character sheet.

This commit is contained in:
Thomas Andres Gomez 2024-11-26 11:50:53 +01:00
parent 52f7f8333b
commit 51021d41d5
37 changed files with 1996 additions and 529 deletions

View file

@ -3,7 +3,6 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.kotlinSerialization)
// alias(libs.plugins.kotlinKtor)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
}
@ -22,6 +21,7 @@ kotlin {
implementation(compose.ui)
implementation(compose.components.resources)
implementation(compose.components.uiToolingPreview)
implementation(libs.compose.desktop.preview)
implementation(libs.androidx.lifecycle.viewmodel)
implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.androidx.navigation.compose)

View file

@ -19,7 +19,7 @@
<string name="character_sheet_edit__title">Création de personnage</string>
<string name="character_sheet_edit__name_placeholder">Nom</string>
<string name="character_sheet_edit__add_roll_action">Ajouter un lancer</string>
<string name="character_sheet_edit__add_roll_action">Ajouter une action</string>
<string name="character_sheet_edit__save_action">Sauvegarder</string>
<string name="character_sheet_edit__characteristics__title">Caractéristiques</string>
<string name="character_sheet_edit__characteristics__str">Force</string>
@ -35,8 +35,15 @@
<string name="character_sheet_edit__sub_characteristics__power_point">Points de pouvoir</string>
<string name="character_sheet_edit__sub_characteristics__damage_bonus">Bonus aux dégats</string>
<string name="character_sheet_edit__sub_characteristics__armor">Armure</string>
<string name="character_sheet_edit__skills__common_title">Compétences communes</string>
<string name="character_sheet_edit__skills__special_title">Compétences spéciales</string>
<string name="character_sheet_edit__skills__special_action">Ajouter une compétence spéciale</string>
<string name="character_sheet_edit__skills__magic_title">Compétences magiques</string>
<string name="character_sheet_edit__skills__magic_action">Ajouter une compétence magique</string>
<string name="character_sheet_edit__skills__base_label">Base</string>
<string name="character_sheet_edit__skills__bonus_label">Bonus</string>
<string name="character_sheet_edit__skills__level_label">Niveau</string>
<string name="character_sheet_edit__skills__title">Compétences</string>
<string name="character_sheet_edit__skills__add_action">Ajouter une compétence</string>
<string name="character_sheet_edit__skills__combat">Bagarre</string>
<string name="character_sheet_edit__skills__dodge">Esquive</string>
<string name="character_sheet_edit__skills__grab">Saisie</string>
@ -57,6 +64,9 @@
<string name="character_sheet_edit__occupation__add_action">Ajouter une occupation</string>
<string name="character_sheet_edit__magic__title">Compétences magiques</string>
<string name="character_sheet_edit__magic__add_action">Ajouter une compétence magique</string>
<string name="character_sheet_edit__delete__label">Supprimer</string>
<string name="character_sheet_edit__occupation__label">Compétence d'occupation</string>
<string name="character_sheet__diminished__label">État diminuer</string>
<string name="character_sheet__edit__label">Modifier</string>
@ -74,9 +84,9 @@
<string name="character_sheet__sub_characteristics__power_point">Points de pouvoir</string>
<string name="character_sheet__sub_characteristics__damage_bonus">Bonus aux dégats</string>
<string name="character_sheet__sub_characteristics__armor">Armure</string>
<string name="character_sheet__skills__title">Compétences</string>
<string name="character_sheet__occupations_title">Occupations</string>
<string name="character_sheet__magics__title">Compétences magiques</string>
<string name="character_sheet__skills__common_title">Compétences communes</string>
<string name="character_sheet__skills__special_title">Compétences spéciales</string>
<string name="character_sheet__skills__magic_title">Compétences magiques</string>
<string name="character_sheet__delete_dialog__title">Supprimer la feuille de personnage</string>
<string name="character_sheet__delete_dialog__description">Êtes-vous sûr de vouloir supprimer "%1$s" ?</string>
<string name="character_sheet__delete_dialog__confirm_action">Confirmer</string>
@ -93,7 +103,7 @@
<string name="tooltip__sub_characteristics__movement">Le Déplacement (DEP) est une valeur de jeu qui détermine la distance que peut parcourir un personnage en un round de combat. Tous les humains ont un DEP de 10. Le DEP a une valeur réelle flexible, mais généralement, chaque point de DEP équivaut à un déplacement dun mètre. En course, un point équivaut à trois mètres.</string>
<string name="tooltip__sub_characteristics__hit_point">Les points de vie (PV) sont égaux à la somme CON+TAI du personnage, divisée par deux (arrondie au supérieur). Ils sont soustraits lorsque le personnage subit des dommages. Quand les points de vie tombent à 0, le personnage sombre dans linconscience. S'il reste inconscient trop longtemps, il meurt. Tous les points de vie régénèrent naturellement après une nuit de repos.</string>
<string name="tooltip__sub_characteristics__power_point">les points de pouvoir sont égaux au POU et sont dépensés pour utiliser la magie ou dautres pouvoirs. Tous les points de pouvoir régénèrent naturellement après une nuit de repos.</string>
<string name="tooltip__sub_characteristics__bonus_damage">Les personnages plus massifs ou plus forts infligent plus de dégâts quand ils frappent leurs ennemis en combat au corps à corps. Le modificateur sapplique aux dégâts infligés par toute attaque portée par les personnages avec des armes de mêlée. La moitié de ce bonus s'applique aux attaques de lancer.</string>
<string name="tooltip__sub_characteristics__bonus_damage">Les personnages plus massifs ou plus forts infligent plus de dégâts quand ils frappent leurs ennemis en combat au corps à corps. Le modificateur sapplique aux dégâts infligés par toute attaque portée par les personnages avec des armes de mêlée (BDC). La moitié de ce bonus s'applique aux attaques de lancer (BDD).</string>
<string name="tooltip__sub_characteristics__armor">Une armure protège son porteur des blessures. Lorsquun personnage est touché en combat par une attaque non magique, soustrayez les points darmure aux points de dégâts infligés. Les dommages au-delà de la protection de larmure surpassent celle-ci et sont infligés au personnage, réduisant ses points de vie actuels.</string>
<string name="tooltip__skills__combat">Attaque en combat à mains nues. Une attaque réussie inflige 1D3 + BDGT.</string>

View file

@ -1,10 +1,14 @@
package com.pixelized.desktop.lwa
import com.pixelized.desktop.lwa.business.DamageBonusUseCase
import com.pixelized.desktop.lwa.business.SkillValueComputationUseCase
import com.pixelized.desktop.lwa.parser.arithmetic.ArithmeticParser
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetJsonFactory
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetStore
import com.pixelized.desktop.lwa.repository.network.NetworkRepository
import com.pixelized.desktop.lwa.repository.roll.RollHistoryRepository
import com.pixelized.desktop.lwa.screen.characterSheet.common.SkillFieldFactory
import com.pixelized.desktop.lwa.screen.characterSheet.detail.CharacterSheetFactory
import com.pixelized.desktop.lwa.screen.characterSheet.detail.CharacterSheetViewModel
import com.pixelized.desktop.lwa.screen.characterSheet.edit.CharacterSheetEditFactory
@ -21,9 +25,11 @@ import org.koin.dsl.module
val moduleDependencies
get() = listOf(
repositoryDependencies,
parserDependencies,
factoryDependencies,
repositoryDependencies,
viewModelDependencies,
useCaseDependencies,
)
val repositoryDependencies
@ -40,6 +46,7 @@ val factoryDependencies
factoryOf(::CharacterSheetEditFactory)
factoryOf(::CharacterSheetJsonFactory)
factoryOf(::NetworkFactory)
factoryOf(::SkillFieldFactory)
}
val viewModelDependencies
@ -50,4 +57,15 @@ val viewModelDependencies
viewModelOf(::RollViewModel)
viewModelOf(::RollHistoryViewModel)
viewModelOf(::NetworkViewModel)
}
}
val parserDependencies
get() = module {
factoryOf(::ArithmeticParser)
}
val useCaseDependencies
get() = module {
factoryOf(::DamageBonusUseCase)
factoryOf(::SkillValueComputationUseCase)
}

View file

@ -1,6 +1,6 @@
package com.pixelized.desktop.lwa.business
object DamageBonusUseCase {
class DamageBonusUseCase {
fun bonusDamage(strength: Int, height: Int): String {
return bonusDamage(stat = strength + height)

View file

@ -0,0 +1,40 @@
package com.pixelized.desktop.lwa.business
import com.pixelized.desktop.lwa.parser.arithmetic.Instruction
import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheet
import kotlin.math.max
class SkillValueComputationUseCase {
fun computeSkillValue(
sheet: CharacterSheet,
skill: CharacterSheet.Skill,
diminished: Int,
): Int {
val baseSum = skill.base.sumOf {
when (val instruction = it.instruction) {
is Instruction.Dice -> 0
is Instruction.Flat -> instruction.value
Instruction.Word.BDC -> 0
Instruction.Word.BDD -> 0
Instruction.Word.STR -> sheet.strength
Instruction.Word.DEX -> sheet.dexterity
Instruction.Word.CON -> sheet.constitution
Instruction.Word.HEI -> sheet.height
Instruction.Word.INT -> sheet.intelligence
Instruction.Word.POW -> sheet.power
Instruction.Word.CHA -> sheet.charisma
} * it.sign
}
val base = if (skill.occupation) {
max(MIN_OCCUPATION_VALUE, baseSum)
} else {
baseSum
}
return max(base + skill.bonus + skill.level - diminished, 0)
}
companion object {
private const val MIN_OCCUPATION_VALUE = 40
}
}

View file

@ -1,19 +1,21 @@
package com.pixelized.desktop.lwa.composable.decoratedBox
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.theme.LwaTheme
import org.jetbrains.compose.ui.tooling.preview.Preview
import com.pixelized.desktop.lwa.utils.preview.ContentPreview
@Composable
fun DecoratedBox(
@ -34,12 +36,14 @@ fun DecoratedBox(
@Composable
@Preview
private fun DecoratedBoxPreview() {
LwaTheme {
Surface {
DecoratedBox {
Text("test")
ContentPreview {
DecoratedBox {
Box(
modifier = Modifier.width(width = 128.dp).height(64.dp),
contentAlignment = Alignment.Center,
) {
Text("Test")
}
}
}
}
}

View file

@ -7,7 +7,6 @@ import androidx.compose.runtime.State
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.rememberWindowState
import com.pixelized.desktop.lwa.navigation.window.destination.Window
val LocalWindow = compositionLocalOf<Window> {
@ -43,10 +42,7 @@ fun WindowsNavHost(
) {
Window(
onCloseRequest = { controller.hideWindow(id = window.id) },
state = rememberWindowState(
width = window.width,
height = window.height,
),
state = window.state,
title = window.title,
) {
content.invoke(window)

View file

@ -1,6 +1,15 @@
package com.pixelized.desktop.lwa.navigation.window.destination
import androidx.compose.runtime.Stable
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
@Stable
class CharacterSheetCreateWindow : Window(title = "")
class CharacterSheetCreateWindow : Window(
title = "",
size = size,
) {
companion object {
val size = DpSize(600.dp, 900.dp)
}
}

View file

@ -1,6 +1,8 @@
package com.pixelized.desktop.lwa.navigation.window.destination
import androidx.compose.runtime.Stable
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
@Stable
class CharacterSheetWindow(
@ -8,5 +10,13 @@ class CharacterSheetWindow(
characterName: String,
) : Window(
title = characterName,
)
size = size,
) {
companion object {
val size = DpSize(
width = 400.dp + 64.dp,
height = 900.dp,
)
}
}

View file

@ -2,13 +2,31 @@ package com.pixelized.desktop.lwa.navigation.window.destination
import androidx.compose.runtime.Stable
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.window.WindowPlacement
import androidx.compose.ui.window.WindowPosition
import androidx.compose.ui.window.WindowState
import java.util.UUID
@Stable
sealed class Window(
val id: String = UUID.randomUUID().toString(),
val title: String,
val width: Dp = 400.dp + 64.dp,
val height: Dp = 900.dp,
)
size: DpSize,
) {
val state = WindowState(
placement = WindowPlacement.Floating,
isMinimized = false,
position = WindowPosition.PlatformDefault,
width = size.width,
height = size.height,
)
val width: Dp get() = state.size.width
val height: Dp get() = state.size.height
val size: DpSize get() = state.size
suspend fun resize(size: DpSize) {
state.size = size
}
}

View file

@ -0,0 +1,112 @@
package com.pixelized.desktop.lwa.parser.arithmetic
import com.pixelized.desktop.lwa.parser.arithmetic.Instruction.Dice.Modifier
import com.pixelized.desktop.lwa.parser.arithmetic.Instruction.Word
// https://medium.com/@gustavoinzunza/how-to-solve-balanced-parenthesis-problem-with-regex-in-ruby-113eff30a79a
// https://stackoverflow.com/questions/546433/regular-expression-to-match-balanced-parentheses
class ArithmeticParser {
private val operatorParser = Regex(
"""\s*(?<operator>[-+])?\s*(?<payload>[^-+]*)"""
)
private val diceParser = Regex(
"""^(?<modifier>[ade])?(?<quantity>\d+)[dD](?<faces>\d+)"""
)
private val wordParser = Regex(
"""^(?<word>${words.joinToString(separator = "|") { it }})"""
)
private val flatParser = Regex(
"""(?<flat>\d+)"""
)
@Throws(Instruction.UnknownInstruction::class)
fun parse(value: String): List<Arithmetic> {
return operatorParser.findAll(value).mapNotNull {
val (operator, instruction) = it.destructured
if (instruction.isNotBlank()) {
Arithmetic(
sign = parseOperator(operator),
instruction = parseInstruction(instruction),
)
} else {
null
}
}.toList()
}
@Throws(Instruction.UnknownInstruction::class)
fun parseInstruction(instruction: String): Instruction {
diceParser.find(instruction)?.let {
val (modifier, quantity, faces) = it.destructured
Instruction.Dice(
modifier = parseModifier(value = modifier),
quantity = quantity.toInt(),
faces = faces.toInt(),
)
}?.let { return it }
wordParser.find(instruction)?.let {
parseWord(value = it.value)
}?.let { return it }
flatParser.find(instruction)?.let {
Instruction.Flat(value = it.value.toInt())
}?.let { return it }
throw Instruction.UnknownInstruction(payload = instruction)
}
private fun parseOperator(operator: String): Int {
return when (operator) {
"-" -> -1
"+" -> 1
else -> 1
}
}
private fun parseModifier(value: String): Modifier? {
return when (value) {
"a" -> Modifier.ADVANTAGE
"d" -> Modifier.DISADVANTAGE
"e" -> Modifier.EMPHASIS
else -> null
}
}
private fun parseWord(value: String): Word? {
return try {
Word.valueOf(value)
} catch (_: Exception) {
return null
}
}
fun convertInstructionToString(
instructions: List<Arithmetic>,
): String {
return instructions.map {
val sign = if (it.sign > 0) "+" else "-"
val value = when (val instruction = it.instruction) {
is Instruction.Dice -> "${instruction.modifier ?: ""}${instruction.quantity}d${instruction.faces}"
is Instruction.Flat -> "${instruction.value}"
Word.BDC -> Word.BDC.name
Word.BDD -> Word.BDD.name
Word.STR -> Word.STR.name
Word.DEX -> Word.DEX.name
Word.CON -> Word.CON.name
Word.HEI -> Word.HEI.name
Word.INT -> Word.INT.name
Word.POW -> Word.POW.name
Word.CHA -> Word.CHA.name
}
"$sign$value"
}.joinToString(separator = " ") {
it
}
}
companion object {
val words: List<String> = Word.entries.map { it.name }
}
}

View file

@ -0,0 +1,40 @@
package com.pixelized.desktop.lwa.parser.arithmetic
class Arithmetic(
val sign: Int,
val instruction: Instruction,
)
sealed interface Instruction {
data class Dice(
val modifier: Modifier?,
val quantity: Int,
val faces: Int,
) : Instruction {
enum class Modifier {
ADVANTAGE,
DISADVANTAGE,
EMPHASIS,
}
}
data class Flat(
val value: Int,
) : Instruction
enum class Word : Instruction {
BDC, // Damages bonus for melee
BDD, // Damages bonus for range
STR, // Strength
DEX, // Dexterity
CON, // Constitution
HEI, // Height
INT, // Intelligence
POW, // Power
CHA, // Charisma
}
class UnknownInstruction(payload: String) : RuntimeException(
"Unknown instruction exception. Unable to parse the following payload:\"$payload\" into an instruction"
)
}

View file

@ -1,12 +1,18 @@
package com.pixelized.desktop.lwa.repository.characterSheet
import com.pixelized.desktop.lwa.business.DamageBonusUseCase
import com.pixelized.desktop.lwa.parser.arithmetic.ArithmeticParser
import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheet
import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheetJson
import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheetJsonV1
import kotlin.math.ceil
class CharacterSheetJsonFactory {
class CharacterSheetJsonFactory(
private val bonusDamageUseCase: DamageBonusUseCase,
private val arithmeticParser: ArithmeticParser,
) {
fun convertLastJsonVersion(
fun convertToJson(
sheet: CharacterSheet,
): CharacterSheetJson {
val json = CharacterSheetJsonV1(
@ -19,38 +25,51 @@ class CharacterSheetJsonFactory {
intelligence = sheet.intelligence,
power = sheet.power,
charisma = sheet.charisma,
movement = sheet.movement,
movement = if (sheet.overrideMovement) sheet.movement else null,
currentHp = sheet.currentHp,
maxHp = sheet.maxHp,
currentPP = sheet.currentPP,
maxPP = sheet.maxPP,
damageBonus = sheet.damageBonus,
armor = sheet.armor,
skills = sheet.skills.map {
maxHp = if (sheet.overrideMaxHp) sheet.maxHp else null,
currentPP = sheet.currentPp,
maxPP = if (sheet.overrideMaxPP) sheet.maxPP else null,
damageBonus = if (sheet.overrideDamageBonus) sheet.damageBonus else null,
armor = if (sheet.overrideArmor) sheet.armor else null,
skills = sheet.commonSkills.map {
CharacterSheetJsonV1.Skill(
id = it.id,
label = it.label,
value = it.value,
base = arithmeticParser.convertInstructionToString(it.base),
bonus = it.bonus,
level = it.level,
occupation = it.occupation,
used = it.used,
)
},
occupations = sheet.occupations.map {
occupations = sheet.specialSkills.map {
CharacterSheetJsonV1.Skill(
id = it.id,
label = it.label,
value = it.value,
base = arithmeticParser.convertInstructionToString(it.base),
bonus = it.bonus,
level = it.level,
occupation = it.occupation,
used = it.used,
)
},
magics = sheet.magics.map {
magics = sheet.magicSkills.map {
CharacterSheetJsonV1.Skill(
id = it.id,
label = it.label,
value = it.value,
base = arithmeticParser.convertInstructionToString(it.base),
bonus = it.bonus,
level = it.level,
occupation = it.occupation,
used = it.used,
)
},
rolls = sheet.rolls.map {
rolls = sheet.actions.map {
CharacterSheetJsonV1.Roll(
id = it.id,
label = it.label,
roll = it.roll,
roll = arithmeticParser.convertInstructionToString(it.roll),
)
},
)
@ -78,38 +97,60 @@ class CharacterSheetJsonFactory {
intelligence = json.intelligence,
power = json.power,
charisma = json.charisma,
movement = json.movement,
overrideMovement = json.movement != null,
movement = json.movement ?: 10,
currentHp = json.currentHp,
maxHp = json.maxHp,
currentPP = json.currentPP,
maxPP = json.maxPP,
damageBonus = json.damageBonus,
armor = json.armor,
skills = json.skills.map {
overrideMaxHp = json.maxHp != null,
maxHp = json.maxHp ?: (ceil((json.constitution + json.height) / 2f).toInt()),
currentPp = json.currentPP,
overrideMaxPP = json.maxPP != null,
maxPP = json.maxPP ?: json.power,
overrideDamageBonus = json.damageBonus != null,
damageBonus = json.damageBonus
?: bonusDamageUseCase.bonusDamage(
strength = json.strength,
height = json.height
),
overrideArmor = json.armor != null,
armor = json.armor ?: 0,
commonSkills = json.skills.map {
CharacterSheet.Skill(
id = it.id,
label = it.label,
value = it.value,
base = arithmeticParser.parse(it.base),
bonus = it.bonus,
level = it.level,
occupation = it.occupation,
used = it.used,
)
},
occupations = json.occupations.map {
specialSkills = json.occupations.map {
CharacterSheet.Skill(
id = it.id,
label = it.label,
value = it.value,
base = arithmeticParser.parse(it.base),
bonus = it.bonus,
level = it.level,
occupation = it.occupation,
used = it.used,
)
},
magics = json.magics.map {
magicSkills = json.magics.map {
CharacterSheet.Skill(
id = it.id,
label = it.label,
value = it.value,
base = arithmeticParser.parse(it.base),
bonus = it.bonus,
level = it.level,
occupation = it.occupation,
used = it.used,
)
},
rolls = json.rolls.map {
actions = json.rolls.map {
CharacterSheet.Roll(
id = it.id,
label = it.label,
roll = it.roll,
roll = arithmeticParser.parse(it.roll),
)
},
)

View file

@ -26,7 +26,7 @@ class CharacterSheetStore(
fun save(sheets: List<CharacterSheet>) {
val json = try {
sheets
.map(factory::convertLastJsonVersion)
.map(factory::convertToJson)
.let(Json::encodeToString)
} catch (exception: Exception) {
throw JsonConversionException(root = exception)

View file

@ -1,5 +1,7 @@
package com.pixelized.desktop.lwa.repository.characterSheet.model
import com.pixelized.desktop.lwa.parser.arithmetic.Arithmetic
data class CharacterSheet(
val id: String,
val name: String,
@ -12,49 +14,57 @@ data class CharacterSheet(
val power: Int,
val charisma: Int,
// sub characteristics
val overrideMovement: Boolean,
val movement: Int,
val currentHp: Int,
val overrideMaxHp: Boolean,
val maxHp: Int,
val currentPP: Int,
val currentPp: Int,
val overrideMaxPP: Boolean,
val maxPP: Int,
val overrideDamageBonus: Boolean,
val damageBonus: String,
val overrideArmor: Boolean,
val armor: Int,
// skills
val skills: List<Skill>,
// occupations
val occupations: List<Skill>,
// magic skill
val magics: List<Skill>,
// attack
val rolls: List<Roll>,
val commonSkills: List<Skill>,
val specialSkills: List<Skill>,
val magicSkills: List<Skill>,
// actions
val actions: List<Roll>,
) {
data class Skill(
val id: String,
val label: String,
val value: Int,
val base: List<Arithmetic>,
val bonus: Int,
val level: Int,
val occupation: Boolean,
val used: Boolean,
)
data class Roll(
val id: String,
val label: String,
val roll: String,
val roll: List<Arithmetic>,
)
companion object {
const val COMBAT = "Bagarre"
const val DODGE = "Esquive"
const val GRAB = "Saisie"
const val THROW = "Lancer"
const val ATHLETICS = "Athlétisme"
const val ACROBATICS = "Acrobatie"
const val PERCEPTION = "Perception"
const val SEARCH = "Recherche"
const val EMPATHY = "Empathie"
const val PERSUASION = "Persuasion"
const val INTIMIDATION = "Intimidation"
const val SPIEL = "Baratin"
const val BARGAIN = "Marchandage"
const val DISCRETION = "Discrétion"
const val SLEIGHT_OF_HAND = "Escamotage"
const val AID = "Premiers soins"
object CommonSkillId {
const val COMBAT_ID = "Bagarre"
const val DODGE_ID = "Esquive"
const val GRAB_ID = "Saisie"
const val THROW_ID = "Lancer"
const val ATHLETICS_ID = "Athlétisme"
const val ACROBATICS_ID = "Acrobatie"
const val PERCEPTION_ID = "Perception"
const val SEARCH_ID = "Recherche"
const val EMPATHY_ID = "Empathie"
const val PERSUASION_ID = "Persuasion"
const val INTIMIDATION_ID = "Intimidation"
const val SPIEL_ID = "Baratin"
const val BARGAIN_ID = "Marchandage"
const val DISCRETION_ID = "Discrétion"
const val SLEIGHT_OF_HAND_ID = "Escamotage"
const val AID_ID = "Premiers soins"
}
}

View file

@ -15,13 +15,13 @@ data class CharacterSheetJsonV1(
val power: Int,
val charisma: Int,
// sub characteristics
val movement: Int,
val movement: Int?,
val currentHp: Int,
val maxHp: Int,
val maxHp: Int?,
val currentPP: Int,
val maxPP: Int,
val damageBonus: String,
val armor: Int,
val maxPP: Int?,
val damageBonus: String?,
val armor: Int?,
// skills
val skills: List<Skill>,
// occupations
@ -33,33 +33,19 @@ data class CharacterSheetJsonV1(
) : CharacterSheetJson {
@Serializable
data class Skill(
val id: String,
val label: String,
val value: Int,
val base: String,
val bonus: Int,
val level: Int,
val occupation: Boolean,
val used: Boolean,
)
@Serializable
data class Roll(
val id: String,
val label: String,
val roll: String,
)
companion object {
const val COMBAT = "Bagarre"
const val DODGE = "Esquive"
const val GRAB = "Saisie"
const val THROW = "Lancer"
const val ATHLETICS = "Athlétisme"
const val ACROBATICS = "Acrobatie"
const val PERCEPTION = "Perception"
const val SEARCH = "Recherche"
const val EMPATHY = "Empathie"
const val PERSUASION = "Persuasion"
const val INTIMIDATION = "Intimidation"
const val SPIEL = "Baratin"
const val BARGAIN = "Marchandage"
const val DISCRETION = "Discrétion"
const val SLEIGHT_OF_HAND = "Escamotage"
const val AID = "Premiers soins"
}
}

View file

@ -0,0 +1,86 @@
package com.pixelized.desktop.lwa.screen.characterSheet.common
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.runtime.mutableStateOf
import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.SkillFieldUio
import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.textfield.TextFieldWrapperUio
import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.option.ActionOption
import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.option.CheckedOption
import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.option.OptionUio
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__delete__label
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__occupation__label
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__base_label
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__bonus_label
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__level_label
import org.jetbrains.compose.resources.getString
import java.util.UUID
class SkillFieldFactory {
suspend fun createSkill(
id: String = UUID.randomUUID().toString(),
label: String,
labelValue: String = "",
baseValue: String = "",
bonusValue: String = "",
levelValue: String = "",
options: List<OptionUio> = emptyList(),
): SkillFieldUio {
return SkillFieldUio(
id = id,
label = createWrapper(
label = label,
value = labelValue,
),
base = createWrapper(
label = getString(Res.string.character_sheet_edit__skills__base_label),
value = baseValue,
),
bonus = createWrapper(
label = getString(Res.string.character_sheet_edit__skills__bonus_label),
value = bonusValue,
),
level = createWrapper(
label = getString(Res.string.character_sheet_edit__skills__level_label),
value = levelValue,
),
options = options,
)
}
fun createWrapper(
enable: Boolean = true,
label: String = "",
value: String = "",
): TextFieldWrapperUio {
val state = mutableStateOf(value)
return TextFieldWrapperUio(
enable = enable,
label = label,
value = state,
onValueChange = { state.value = it },
)
}
suspend fun deleteOption(onDelete: () -> Unit) = ActionOption.DeleteOptionUio(
icon = Icons.Default.Delete,
label = getString(Res.string.character_sheet_edit__delete__label),
onOption = onDelete,
)
suspend fun occupationOption(checked: Boolean) = mutableStateOf(checked).let { state ->
CheckedOption.OccupationOption(
checked = state,
label = getString(Res.string.character_sheet_edit__occupation__label),
onOption = { state.value = state.value.not() },
)
}
}
val List<OptionUio>.occupation: Boolean
get() = this.firstNotNullOfOrNull {
if (it is CheckedOption.OccupationOption) it.checked.value else null
} ?: false

View file

@ -1,7 +1,9 @@
package com.pixelized.desktop.lwa.screen.characterSheet.detail
import com.pixelized.desktop.lwa.business.SkillValueComputationUseCase
import com.pixelized.desktop.lwa.composable.tooltip.TooltipUio
import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheet
import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheet.CommonSkillId
import com.pixelized.desktop.lwa.screen.characterSheet.detail.CharacterSheetPageUio.Characteristic
import com.pixelized.desktop.lwa.screen.characterSheet.detail.CharacterSheetPageUio.Node
import lwacharactersheet.composeapp.generated.resources.Res
@ -46,9 +48,10 @@ import lwacharactersheet.composeapp.generated.resources.tooltip__sub_characteris
import lwacharactersheet.composeapp.generated.resources.tooltip__sub_characteristics__movement
import lwacharactersheet.composeapp.generated.resources.tooltip__sub_characteristics__power_point
import org.jetbrains.compose.resources.getString
import kotlin.math.max
class CharacterSheetFactory {
class CharacterSheetFactory(
private val skillUseCase: SkillValueComputationUseCase,
) {
companion object {
const val HP = "HP"
@ -56,17 +59,17 @@ class CharacterSheetFactory {
}
suspend fun convertToUio(
model: CharacterSheet,
sheet: CharacterSheet,
diminished: Int,
): CharacterSheetPageUio {
return CharacterSheetPageUio(
id = model.id,
name = model.name,
id = sheet.id,
name = sheet.name,
characteristics = listOf(
Characteristic(
id = "STR",
label = getString(Res.string.character_sheet__characteristics__str),
value = "${model.strength}",
value = "${sheet.strength}",
tooltips = TooltipUio(
title = getString(Res.string.character_sheet__characteristics__str),
description = getString(Res.string.tooltip__characteristics__strength),
@ -76,7 +79,7 @@ class CharacterSheetFactory {
Characteristic(
id = "DEX",
label = getString(Res.string.character_sheet__characteristics__dex),
value = "${model.dexterity}",
value = "${sheet.dexterity}",
tooltips = TooltipUio(
title = getString(Res.string.character_sheet__characteristics__dex),
description = getString(Res.string.tooltip__characteristics__dexterity),
@ -86,7 +89,7 @@ class CharacterSheetFactory {
Characteristic(
id = "CON",
label = getString(Res.string.character_sheet__characteristics__con),
value = "${model.constitution}",
value = "${sheet.constitution}",
tooltips = TooltipUio(
title = getString(Res.string.character_sheet__characteristics__con),
description = getString(Res.string.tooltip__characteristics__constitution),
@ -96,7 +99,7 @@ class CharacterSheetFactory {
Characteristic(
id = "HEI",
label = getString(Res.string.character_sheet__characteristics__hei),
value = "${model.height}",
value = "${sheet.height}",
tooltips = TooltipUio(
title = getString(Res.string.character_sheet__characteristics__hei),
description = getString(Res.string.tooltip__characteristics__height),
@ -106,7 +109,7 @@ class CharacterSheetFactory {
Characteristic(
id = "INT",
label = getString(Res.string.character_sheet__characteristics__int),
value = "${model.intelligence}",
value = "${sheet.intelligence}",
tooltips = TooltipUio(
title = getString(Res.string.character_sheet__characteristics__int),
description = getString(Res.string.tooltip__characteristics__intelligence),
@ -116,7 +119,7 @@ class CharacterSheetFactory {
Characteristic(
id = "POW",
label = getString(Res.string.character_sheet__characteristics__pow),
value = "${model.power}",
value = "${sheet.power}",
tooltips = TooltipUio(
title = getString(Res.string.character_sheet__characteristics__pow),
description = getString(Res.string.tooltip__characteristics__power),
@ -126,7 +129,7 @@ class CharacterSheetFactory {
Characteristic(
id = "CHA",
label = getString(Res.string.character_sheet__characteristics__cha),
value = "${model.charisma}",
value = "${sheet.charisma}",
tooltips = TooltipUio(
title = getString(Res.string.character_sheet__characteristics__cha),
description = getString(Res.string.tooltip__characteristics__charisma),
@ -138,7 +141,7 @@ class CharacterSheetFactory {
Characteristic(
id = "MOV",
label = getString(Res.string.character_sheet__sub_characteristics__movement),
value = "${model.movement}",
value = "${sheet.movement}",
tooltips = TooltipUio(
title = getString(Res.string.character_sheet__sub_characteristics__movement),
description = getString(Res.string.tooltip__sub_characteristics__movement),
@ -148,7 +151,7 @@ class CharacterSheetFactory {
Characteristic(
id = HP,
label = getString(Res.string.character_sheet__sub_characteristics__hit_point),
value = "${model.currentHp}/${model.maxHp}",
value = "${sheet.currentHp}/${sheet.maxHp}",
tooltips = TooltipUio(
title = getString(Res.string.character_sheet__sub_characteristics__hit_point),
description = getString(Res.string.tooltip__sub_characteristics__hit_point),
@ -158,7 +161,7 @@ class CharacterSheetFactory {
Characteristic(
id = PP,
label = getString(Res.string.character_sheet__sub_characteristics__power_point),
value = "${model.currentPP}/${model.maxPP}",
value = "${sheet.currentPp}/${sheet.maxPP}",
tooltips = TooltipUio(
title = getString(Res.string.character_sheet__sub_characteristics__power_point),
description = getString(Res.string.tooltip__sub_characteristics__power_point),
@ -168,7 +171,7 @@ class CharacterSheetFactory {
Characteristic(
id = "DMG",
label = getString(Res.string.character_sheet__sub_characteristics__damage_bonus),
value = model.damageBonus,
value = sheet.damageBonus,
tooltips = TooltipUio(
title = getString(Res.string.character_sheet__sub_characteristics__damage_bonus),
description = getString(Res.string.tooltip__sub_characteristics__bonus_damage),
@ -178,7 +181,7 @@ class CharacterSheetFactory {
Characteristic(
id = "ARMOR",
label = getString(Res.string.character_sheet__sub_characteristics__armor),
value = "${model.armor}",
value = "${sheet.armor}",
tooltips = TooltipUio(
title = getString(Res.string.character_sheet__sub_characteristics__armor),
description = getString(Res.string.tooltip__sub_characteristics__armor),
@ -186,69 +189,69 @@ class CharacterSheetFactory {
editable = false,
),
),
skills = model.skills.mapNotNull { skill ->
if (skill.value > 0) {
val description = when (skill.label) {
CharacterSheet.COMBAT -> getString(Res.string.tooltip__skills__combat)
CharacterSheet.DODGE -> getString(Res.string.tooltip__skills__dodge)
CharacterSheet.GRAB -> getString(Res.string.tooltip__skills__grab)
CharacterSheet.THROW -> getString(Res.string.tooltip__skills__throw)
CharacterSheet.ATHLETICS -> getString(Res.string.tooltip__skills__athletics)
CharacterSheet.ACROBATICS -> getString(Res.string.tooltip__skills__acrobatics)
CharacterSheet.PERCEPTION -> getString(Res.string.tooltip__skills__perception)
CharacterSheet.SEARCH -> getString(Res.string.tooltip__skills__search)
CharacterSheet.EMPATHY -> getString(Res.string.tooltip__skills__empathy)
CharacterSheet.PERSUASION -> getString(Res.string.tooltip__skills__persuasion)
CharacterSheet.INTIMIDATION -> getString(Res.string.tooltip__skills__intimidation)
CharacterSheet.SPIEL -> getString(Res.string.tooltip__skills__spiel)
CharacterSheet.BARGAIN -> getString(Res.string.tooltip__skills__bargain)
CharacterSheet.DISCRETION -> getString(Res.string.tooltip__skills__discretion)
CharacterSheet.SLEIGHT_OF_HAND -> getString(Res.string.tooltip__skills__sleight_of_hand)
CharacterSheet.AID -> getString(Res.string.tooltip__skills__aid)
else -> null
}
Node(
label = skill.label,
value = skill.value.diminished(diminished),
tooltips = description?.let {
TooltipUio(
title = skill.label,
description = it,
)
},
used = skill.used,
)
} else {
null
commonSkills = sheet.commonSkills.map { skill ->
val description = when (skill.id) {
CommonSkillId.COMBAT_ID -> getString(Res.string.tooltip__skills__combat)
CommonSkillId.DODGE_ID -> getString(Res.string.tooltip__skills__dodge)
CommonSkillId.GRAB_ID -> getString(Res.string.tooltip__skills__grab)
CommonSkillId.THROW_ID -> getString(Res.string.tooltip__skills__throw)
CommonSkillId.ATHLETICS_ID -> getString(Res.string.tooltip__skills__athletics)
CommonSkillId.ACROBATICS_ID -> getString(Res.string.tooltip__skills__acrobatics)
CommonSkillId.PERCEPTION_ID -> getString(Res.string.tooltip__skills__perception)
CommonSkillId.SEARCH_ID -> getString(Res.string.tooltip__skills__search)
CommonSkillId.EMPATHY_ID -> getString(Res.string.tooltip__skills__empathy)
CommonSkillId.PERSUASION_ID -> getString(Res.string.tooltip__skills__persuasion)
CommonSkillId.INTIMIDATION_ID -> getString(Res.string.tooltip__skills__intimidation)
CommonSkillId.SPIEL_ID -> getString(Res.string.tooltip__skills__spiel)
CommonSkillId.BARGAIN_ID -> getString(Res.string.tooltip__skills__bargain)
CommonSkillId.DISCRETION_ID -> getString(Res.string.tooltip__skills__discretion)
CommonSkillId.SLEIGHT_OF_HAND_ID -> getString(Res.string.tooltip__skills__sleight_of_hand)
CommonSkillId.AID_ID -> getString(Res.string.tooltip__skills__aid)
else -> null
}
Node(
label = skill.label,
value = skillUseCase.computeSkillValue(
sheet = sheet,
skill = skill,
diminished = diminished,
),
tooltips = description?.let {
TooltipUio(
title = skill.label,
description = it,
)
},
used = skill.used,
)
},
occupations = model.occupations.mapNotNull {
if (it.value > 0) {
Node(
label = it.label,
value = it.value.diminished(diminished),
used = it.used,
)
} else {
null
}
specialSKills = sheet.specialSkills.map {
Node(
label = it.label,
value = skillUseCase.computeSkillValue(
sheet = sheet,
skill = it,
diminished = diminished,
),
used = it.used,
)
},
magics = model.magics.mapNotNull {
if (it.value > 0) {
Node(
label = it.label,
value = it.value.diminished(diminished),
used = it.used,
)
} else {
null
}
magicsSkills = sheet.magicSkills.map {
Node(
label = it.label,
value = skillUseCase.computeSkillValue(
sheet = sheet,
skill = it,
diminished = diminished,
),
used = it.used,
)
},
rolls = model.rolls.mapNotNull {
actions = sheet.actions.mapNotNull {
if (it.roll.isNotEmpty()) {
CharacterSheetPageUio.Roll(
label = it.label,
value = it.roll,
value = "TODO",//it.roll,
)
} else {
null
@ -256,8 +259,4 @@ class CharacterSheetFactory {
}
)
}
}
fun Int.diminished(value: Int): Int {
return max(0, this - value)
}

View file

@ -41,6 +41,7 @@ import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
@ -61,6 +62,7 @@ import com.pixelized.desktop.lwa.composable.tooltip.TooltipUio
import com.pixelized.desktop.lwa.navigation.screen.LocalScreenController
import com.pixelized.desktop.lwa.navigation.screen.destination.navigateToCharacterSheetEdit
import com.pixelized.desktop.lwa.navigation.window.LocalWindow
import com.pixelized.desktop.lwa.navigation.window.destination.CharacterSheetWindow
import com.pixelized.desktop.lwa.screen.characterSheet.detail.dialog.CharacterSheetDeleteConfirmationDialog
import com.pixelized.desktop.lwa.screen.characterSheet.detail.dialog.CharacterSheetStatDialog
import com.pixelized.desktop.lwa.screen.characterSheet.detail.dialog.DiminishedStatDialog
@ -70,9 +72,9 @@ import kotlinx.coroutines.launch
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.character_sheet__delete__label
import lwacharactersheet.composeapp.generated.resources.character_sheet__edit__label
import lwacharactersheet.composeapp.generated.resources.character_sheet__magics__title
import lwacharactersheet.composeapp.generated.resources.character_sheet__occupations_title
import lwacharactersheet.composeapp.generated.resources.character_sheet__skills__title
import lwacharactersheet.composeapp.generated.resources.character_sheet__skills__common_title
import lwacharactersheet.composeapp.generated.resources.character_sheet__skills__magic_title
import lwacharactersheet.composeapp.generated.resources.character_sheet__skills__special_title
import lwacharactersheet.composeapp.generated.resources.character_sheet__sub_characteristics__title
import lwacharactersheet.composeapp.generated.resources.ic_d20_32dp
import lwacharactersheet.composeapp.generated.resources.ic_skull_32dp
@ -86,10 +88,10 @@ data class CharacterSheetPageUio(
val name: String,
val characteristics: List<Characteristic>,
val subCharacteristics: List<Characteristic>,
val skills: List<Node>,
val occupations: List<Node>,
val magics: List<Node>,
val rolls: List<Roll>,
val commonSkills: List<Node>,
val specialSKills: List<Node>,
val magicsSkills: List<Node>,
val actions: List<Roll>,
) {
@Stable
data class Characteristic(
@ -126,6 +128,10 @@ fun CharacterSheetPage(
val scope = rememberCoroutineScope()
val blurController = remember { BlurContentController() }
LaunchedEffect(Unit) {
window.resize(size = CharacterSheetWindow.size)
}
Surface(
modifier = Modifier.fillMaxSize(),
) {
@ -365,7 +371,7 @@ fun CharacterSheetPageContent(
modifier = Modifier
.verticalScroll(state = scrollState)
.padding(paddingValues)
.padding(horizontal = 24.dp, vertical = 16.dp),
.padding(all = 16.dp),
horizontalArrangement = Arrangement.spacedBy(space = 16.dp),
) {
Column(
@ -427,9 +433,9 @@ fun CharacterSheetPageContent(
.padding(bottom = 8.dp),
style = MaterialTheme.typography.caption,
textAlign = TextAlign.Center,
text = stringResource(Res.string.character_sheet__skills__title),
text = stringResource(Res.string.character_sheet__skills__common_title),
)
characterSheet.skills.forEach { skill ->
characterSheet.commonSkills.forEach { skill ->
Skill(
modifier = Modifier.cell(),
node = skill,
@ -449,9 +455,9 @@ fun CharacterSheetPageContent(
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp),
style = MaterialTheme.typography.caption,
textAlign = TextAlign.Center,
text = stringResource(Res.string.character_sheet__occupations_title),
text = stringResource(Res.string.character_sheet__skills__special_title),
)
characterSheet.occupations.forEach { occupation ->
characterSheet.specialSKills.forEach { occupation ->
Skill(
modifier = Modifier.cell(),
node = occupation,
@ -471,9 +477,9 @@ fun CharacterSheetPageContent(
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp),
style = MaterialTheme.typography.caption,
textAlign = TextAlign.Center,
text = stringResource(Res.string.character_sheet__magics__title),
text = stringResource(Res.string.character_sheet__skills__magic_title),
)
characterSheet.magics.forEach { magic ->
characterSheet.magicsSkills.forEach { magic ->
Skill(
modifier = Modifier.cell(),
node = magic,
@ -483,7 +489,7 @@ fun CharacterSheetPageContent(
}
}
}
characterSheet.rolls.forEach { roll ->
characterSheet.actions.forEach { roll ->
Roll(
modifier = Modifier.cell(),
label = roll.label,

View file

@ -73,7 +73,7 @@ class CharacterSheetViewModel(
}
).collectAsState { (sheet, diminished) ->
sheet?.let { model ->
runBlocking { factory.convertToUio(model = model, diminished = diminished) }
runBlocking { factory.convertToUio(sheet = model, diminished = diminished) }
}
}
@ -84,21 +84,21 @@ class CharacterSheetViewModel(
fun onUseSkill(skill: CharacterSheetPageUio.Node) {
repository.characterSheetFlow(id = argument.id).value?.let { sheet ->
val skills = sheet.skills.map {
val skills = sheet.commonSkills.map {
if (it.label == skill.label) it.copy(used = it.used.not()) else it
}
val occupations = sheet.occupations.map {
val occupations = sheet.specialSkills.map {
if (it.label == skill.label) it.copy(used = it.used.not()) else it
}
val magics = sheet.magics.map {
val magics = sheet.magicSkills.map {
if (it.label == skill.label) it.copy(used = it.used.not()) else it
}
repository.save(
characterSheet = sheet.copy(
skills = skills,
occupations = occupations,
magics = magics,
commonSkills = skills,
specialSkills = occupations,
magicSkills = magics,
)
)
}
@ -137,7 +137,7 @@ class CharacterSheetViewModel(
CharacterSheetFactory.PP -> {
val value = mutableStateOf(
"${sheet.currentPP}".let {
"${sheet.currentPp}".let {
TextFieldValue(text = it, selection = TextRange(it.length))
}
)
@ -166,7 +166,7 @@ class CharacterSheetViewModel(
val sheet = repository.characterSheetFlow(id = argument.id).value
val updated = when (characteristicId) {
CharacterSheetFactory.HP -> sheet?.copy(currentHp = max(0, min(sheet.maxHp, value)))
CharacterSheetFactory.PP -> sheet?.copy(currentPP = max(0, min(sheet.maxPP, value)))
CharacterSheetFactory.PP -> sheet?.copy(currentPp = max(0, min(sheet.maxPP, value)))
else -> null
}
updated?.let {

View file

@ -1,9 +1,16 @@
package com.pixelized.desktop.lwa.screen.characterSheet.edit
import com.pixelized.desktop.lwa.business.DamageBonusUseCase.bonusDamage
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import com.pixelized.desktop.lwa.business.DamageBonusUseCase
import com.pixelized.desktop.lwa.business.SkillNormalizerUseCase.normalize
import com.pixelized.desktop.lwa.parser.arithmetic.Arithmetic
import com.pixelized.desktop.lwa.parser.arithmetic.ArithmeticParser
import com.pixelized.desktop.lwa.parser.arithmetic.Instruction
import com.pixelized.desktop.lwa.repository.characterSheet.model.CharacterSheet
import com.pixelized.desktop.lwa.screen.characterSheet.edit.CharacterSheetEditPageUio.SkillGroup
import com.pixelized.desktop.lwa.screen.characterSheet.common.SkillFieldFactory
import com.pixelized.desktop.lwa.screen.characterSheet.common.occupation
import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.BaseSkillFieldUio
import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.FieldUio
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__characteristics__cha
@ -13,86 +20,125 @@ import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__ch
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__characteristics__int
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__characteristics__pow
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__characteristics__str
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__characteristics__title
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__magic__add_action
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__magic__title
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__name_placeholder
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__occupation__add_action
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__occupation__title
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__acrobatics
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__add_action
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__aid
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__athletics
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__bargain
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__bonus_label
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__combat
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__discretion
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__dodge
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__empathy
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__grab
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__intimidation
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__level_label
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__magic_title
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__perception
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__persuasion
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__search
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__sleight_of_hand
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__special_title
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__spiel
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__throw
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__title
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__sub_characteristics__armor
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__sub_characteristics__damage_bonus
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__sub_characteristics__hit_point
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__sub_characteristics__movement
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__sub_characteristics__power_point
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__sub_characteristics__title
import org.jetbrains.compose.resources.getString
import java.util.UUID
import kotlin.math.ceil
import kotlin.math.max
import kotlin.math.min
class CharacterSheetEditFactory {
fun convertToModel(sheet: CharacterSheetEditPageUio): CharacterSheet {
class CharacterSheetEditFactory(
private val bonusDamageUseCase: DamageBonusUseCase,
private val skillFactory: SkillFieldFactory,
private val parser: ArithmeticParser,
) {
fun updateCharacterSheet(
currentSheet: CharacterSheet?,
editedSheet: CharacterSheetEditPageUio,
): CharacterSheet {
return CharacterSheet(
id = sheet.id,
name = sheet.name.value.value,
strength = sheet.skills[0].fields[0].unpack(),
dexterity = sheet.skills[0].fields[1].unpack(),
constitution = sheet.skills[0].fields[2].unpack(),
height = sheet.skills[0].fields[3].unpack(),
intelligence = sheet.skills[0].fields[4].unpack(),
power = sheet.skills[0].fields[5].unpack(),
charisma = sheet.skills[0].fields[6].unpack(),
movement = sheet.skills[1].fields[0].unpack(),
currentHp = sheet.skills[1].fields[1].unpack(),
maxHp = sheet.skills[1].fields[1].unpack(),
currentPP = sheet.skills[1].fields[2].unpack(),
maxPP = sheet.skills[1].fields[2].unpack(),
damageBonus = sheet.skills[1].fields[3].unpack(),
armor = sheet.skills[1].fields[4].unpack(),
skills = sheet.skills[2].fields.map {
id = editedSheet.id,
name = editedSheet.name.value.value,
strength = editedSheet.strength.unpack(),
dexterity = editedSheet.dexterity.unpack(),
constitution = editedSheet.constitution.unpack(),
height = editedSheet.height.unpack(),
intelligence = editedSheet.intelligence.unpack(),
power = editedSheet.power.unpack(),
charisma = editedSheet.charisma.unpack(),
overrideMovement = editedSheet.movement.value.value.isNotBlank(),
movement = editedSheet.movement.unpack(),
overrideMaxHp = editedSheet.maxHp.value.value.isNotBlank(),
maxHp = editedSheet.maxHp.unpack(),
currentHp = editedSheet.maxHp.unpack<Int>().let {
max(0, min(it, currentSheet?.currentHp ?: it))
},
overrideMaxPP = editedSheet.maxPP.value.value.isNotBlank(),
maxPP = editedSheet.maxPP.unpack(),
currentPp = editedSheet.maxPP.unpack<Int>().let {
max(0, min(it, currentSheet?.currentPp ?: it))
},
overrideDamageBonus = editedSheet.damageBonus.value.value.isNotBlank(),
damageBonus = editedSheet.damageBonus.unpack(),
overrideArmor = editedSheet.armor.value.value.isNotBlank(),
armor = editedSheet.armor.unpack(),
commonSkills = editedSheet.commonSkills.map { editedSkill ->
val currentSkill = currentSheet?.commonSkills?.firstOrNull {
it.id == editedSkill.id
}
CharacterSheet.Skill(
label = it.label.value,
value = it.unpack(),
used = false,
id = editedSkill.id,
label = editedSkill.label,
base = listOf(
Arithmetic(
sign = 1,
instruction = Instruction.Flat(editedSkill.base.value)
)
),
bonus = editedSkill.bonus.value.value.toIntOrNull() ?: 0,
level = editedSkill.level.value.value.toIntOrNull() ?: 0,
occupation = editedSkill.option.checked.value,
used = currentSkill?.used ?: false,
)
},
occupations = sheet.skills[3].fields.map {
specialSkills = editedSheet.specialSkills.map { editedSkill ->
val currentSkill = currentSheet?.specialSkills?.firstOrNull {
it.id == editedSkill.id
}
CharacterSheet.Skill(
label = it.label.value,
value = it.unpack(),
used = false,
id = editedSkill.id,
label = editedSkill.label.value.value,
base = parser.parse(editedSkill.base.value.value),
bonus = editedSkill.bonus.value.value.toIntOrNull() ?: 0,
level = editedSkill.level.value.value.toIntOrNull() ?: 0,
occupation = editedSkill.options.occupation,
used = currentSkill?.used ?: false,
)
},
magics = sheet.skills[4].fields.map {
magicSkills = editedSheet.magicSkills.map { editedSkill ->
val currentSkill = currentSheet?.magicSkills?.firstOrNull {
it.id == editedSkill.id
}
CharacterSheet.Skill(
label = it.label.value,
value = it.unpack(),
used = false,
id = editedSkill.id,
label = editedSkill.label.value.value,
base = parser.parse(editedSkill.base.value.value),
bonus = editedSkill.bonus.value.value.toIntOrNull() ?: 0,
level = editedSkill.level.value.value.toIntOrNull() ?: 0,
occupation = editedSkill.options.occupation,
used = currentSkill?.used ?: false,
)
},
rolls = sheet.rolls.map {
actions = editedSheet.actions.map {
CharacterSheet.Roll(
id = "", // TODO
label = it.label.value,
roll = it.unpack(),
roll = parser.parse(value = it.unpack()),
)
},
)
@ -100,41 +146,42 @@ class CharacterSheetEditFactory {
suspend fun convertToUio(
sheet: CharacterSheet?,
onDeleteSkill: (skillId: String) -> Unit,
): CharacterSheetEditPageUio {
val str = FieldUio.create(
initialLabel = getString(Res.string.character_sheet_edit__characteristics__str),
initialValue = sheet?.strength?.toString() ?: "",
valuePlaceHolder = { "0" },
valuePlaceHolder = { "10" },
)
val dex = FieldUio.create(
initialLabel = getString(Res.string.character_sheet_edit__characteristics__dex),
initialValue = sheet?.dexterity?.toString() ?: "",
valuePlaceHolder = { "0" }
valuePlaceHolder = { "11" }
)
val con = FieldUio.create(
initialLabel = getString(Res.string.character_sheet_edit__characteristics__con),
initialValue = sheet?.constitution?.toString() ?: "",
valuePlaceHolder = { "0" }
valuePlaceHolder = { "15" }
)
val hei = FieldUio.create(
initialLabel = getString(Res.string.character_sheet_edit__characteristics__hei),
initialValue = sheet?.height?.toString() ?: "",
valuePlaceHolder = { "0" }
valuePlaceHolder = { "13" }
)
val int = FieldUio.create(
initialLabel = getString(Res.string.character_sheet_edit__characteristics__int),
initialValue = sheet?.intelligence?.toString() ?: "",
valuePlaceHolder = { "0" }
valuePlaceHolder = { "9" }
)
val pow = FieldUio.create(
initialLabel = getString(Res.string.character_sheet_edit__characteristics__pow),
initialValue = sheet?.power?.toString() ?: "",
valuePlaceHolder = { "0" }
valuePlaceHolder = { "15" }
)
val cha = FieldUio.create(
initialLabel = getString(Res.string.character_sheet_edit__characteristics__cha),
initialValue = sheet?.charisma?.toString() ?: "",
valuePlaceHolder = { "0" }
valuePlaceHolder = { "7" }
)
fun str(): Int = str.unpack() ?: 0
@ -145,6 +192,9 @@ class CharacterSheetEditFactory {
fun pow(): Int = pow.unpack() ?: 0
fun cha(): Int = cha.unpack() ?: 0
val specialSkillsLabel = getString(Res.string.character_sheet_edit__skills__special_title)
val magicSkillsLabel = getString(Res.string.character_sheet_edit__skills__magic_title)
return CharacterSheetEditPageUio(
id = sheet?.id ?: UUID.randomUUID().toString(),
name = FieldUio.create(
@ -152,153 +202,207 @@ class CharacterSheetEditFactory {
initialLabel = getString(Res.string.character_sheet_edit__name_placeholder),
initialValue = sheet?.name ?: ""
),
skills = listOf(
SkillGroup(
type = SkillGroup.Type.CHARACTERISTICS,
title = getString(Res.string.character_sheet_edit__characteristics__title),
fields = listOf(str, dex, con, hei, int, pow, cha),
strength = str,
dexterity = dex,
constitution = con,
height = hei,
intelligence = int,
power = pow,
charisma = cha,
movement = FieldUio.create(
initialLabel = getString(Res.string.character_sheet_edit__sub_characteristics__movement),
initialValue = (if (sheet?.overrideMovement == true) "${sheet.movement}" else null)
?: "",
valuePlaceHolder = { "10" }
),
maxHp = FieldUio.create(
initialLabel = getString(Res.string.character_sheet_edit__sub_characteristics__hit_point),
initialValue = (if (sheet?.overrideMaxHp == true) "${sheet.maxHp}" else null) ?: "",
valuePlaceHolder = { "${ceil((con() + hei()) / 2f).toInt()}" }
),
maxPP = FieldUio.create(
initialLabel = getString(Res.string.character_sheet_edit__sub_characteristics__power_point),
initialValue = (if (sheet?.overrideMaxPP == true) "${sheet.maxPP}" else null) ?: "",
valuePlaceHolder = { "${pow()}" }
),
damageBonus = FieldUio.create(
initialLabel = getString(Res.string.character_sheet_edit__sub_characteristics__damage_bonus),
initialValue = (if (sheet?.overrideDamageBonus == true) sheet.damageBonus else null)
?: "",
valuePlaceHolder = {
bonusDamageUseCase.bonusDamage(
strength = str(),
height = hei()
)
}
),
armor = FieldUio.create(
initialLabel = getString(Res.string.character_sheet_edit__sub_characteristics__armor),
initialValue = (if (sheet?.overrideArmor == true) sheet.armor.toString() else null)
?: "",
valuePlaceHolder = { "0" }
),
commonSkills = listOf(
createBaseSkill(
sheet = sheet,
id = CharacterSheet.CommonSkillId.COMBAT_ID,
label = getString(Res.string.character_sheet_edit__skills__combat),
base = derivedStateOf { normalize(dex() * 2) },
),
SkillGroup(
type = SkillGroup.Type.SUB_CHARACTERISTICS,
title = getString(Res.string.character_sheet_edit__sub_characteristics__title),
fields = listOf(
FieldUio.create(
initialLabel = getString(Res.string.character_sheet_edit__sub_characteristics__movement),
initialValue = sheet?.movement?.toString() ?: "",
valuePlaceHolder = { "10" }
),
FieldUio.create(
initialLabel = getString(Res.string.character_sheet_edit__sub_characteristics__hit_point),
initialValue = sheet?.maxHp?.toString() ?: "",
valuePlaceHolder = { "${ceil((con() + hei()) / 2f).toInt()}" }
),
FieldUio.create(
initialLabel = getString(Res.string.character_sheet_edit__sub_characteristics__power_point),
initialValue = sheet?.maxPP?.toString() ?: "",
valuePlaceHolder = { "${pow()}" }
),
FieldUio.create(
initialLabel = getString(Res.string.character_sheet_edit__sub_characteristics__damage_bonus),
initialValue = sheet?.damageBonus ?: "",
valuePlaceHolder = { bonusDamage(strength = str(), height = hei()) }
),
FieldUio.create(
initialLabel = getString(Res.string.character_sheet_edit__sub_characteristics__armor),
initialValue = sheet?.armor?.toString() ?: "",
valuePlaceHolder = { "0" }
),
),
createBaseSkill(
sheet = sheet,
id = CharacterSheet.CommonSkillId.DODGE_ID,
label = getString(Res.string.character_sheet_edit__skills__dodge),
base = derivedStateOf { normalize(dex() * 2) },
),
SkillGroup(
type = SkillGroup.Type.SKILLS,
title = getString(Res.string.character_sheet_edit__skills__title),
action = getString(Res.string.character_sheet_edit__skills__add_action),
fields = sheet?.skills?.map {
FieldUio.create(
initialLabel = it.label,
initialValue = it.value.toString(),
)
} ?: listOf(
FieldUio.create(
initialLabel = getString(Res.string.character_sheet_edit__skills__combat),
valuePlaceHolder = { "${normalize(dex() * 2)}" },
),
FieldUio.create(
initialLabel = getString(Res.string.character_sheet_edit__skills__dodge),
valuePlaceHolder = { "${normalize(dex() * 2)}" },
),
FieldUio.create(
initialLabel = getString(Res.string.character_sheet_edit__skills__grab),
valuePlaceHolder = { "${normalize(str() + hei())}" },
),
FieldUio.create(
initialLabel = getString(Res.string.character_sheet_edit__skills__throw),
valuePlaceHolder = { "${normalize(str() + dex())}" },
),
FieldUio.create(
initialLabel = getString(Res.string.character_sheet_edit__skills__athletics),
valuePlaceHolder = { "${normalize(str() + con() * 2)}" },
),
FieldUio.create(
initialLabel = getString(Res.string.character_sheet_edit__skills__acrobatics),
valuePlaceHolder = { "${normalize(dex() + con() * 2)}" },
),
FieldUio.create(
initialLabel = getString(Res.string.character_sheet_edit__skills__perception),
valuePlaceHolder = { "${normalize(10 + int() * 2)}" },
),
FieldUio.create(
initialLabel = getString(Res.string.character_sheet_edit__skills__search),
valuePlaceHolder = { "${normalize(10 + int() * 2)}" },
),
FieldUio.create(
initialLabel = getString(Res.string.character_sheet_edit__skills__empathy),
valuePlaceHolder = { "${normalize(cha() + int())}" },
),
FieldUio.create(
initialLabel = getString(Res.string.character_sheet_edit__skills__persuasion),
valuePlaceHolder = { "${normalize(cha() * 3)}" },
),
FieldUio.create(
initialLabel = getString(Res.string.character_sheet_edit__skills__intimidation),
valuePlaceHolder = { "${normalize(cha() + max(pow(), hei()) * 2)}" },
),
FieldUio.create(
initialLabel = getString(Res.string.character_sheet_edit__skills__spiel),
valuePlaceHolder = { "${normalize(cha() * 2 + int())}" },
),
FieldUio.create(
initialLabel = getString(Res.string.character_sheet_edit__skills__bargain),
valuePlaceHolder = { "${normalize(cha() * 2)}" },
),
FieldUio.create(
initialLabel = getString(Res.string.character_sheet_edit__skills__discretion),
valuePlaceHolder = { "${normalize(cha() + dex() * 2 - hei())}" },
),
FieldUio.create(
initialLabel = getString(Res.string.character_sheet_edit__skills__sleight_of_hand),
valuePlaceHolder = { "${normalize(dex() * 2)}" },
),
FieldUio.create(
initialLabel = getString(Res.string.character_sheet_edit__skills__aid),
valuePlaceHolder = { "${normalize(int() + dex())}" },
),
),
createBaseSkill(
sheet = sheet,
id = CharacterSheet.CommonSkillId.GRAB_ID,
label = getString(Res.string.character_sheet_edit__skills__grab),
base = derivedStateOf { normalize(str() + hei()) },
),
SkillGroup(
type = SkillGroup.Type.OCCUPATIONS,
title = getString(Res.string.character_sheet_edit__occupation__title),
action = getString(Res.string.character_sheet_edit__occupation__add_action),
fields = sheet?.occupations?.map {
FieldUio.create(
initialLabel = it.label,
initialValue = it.value.toString(),
valuePlaceHolder = { "40" }
)
} ?: emptyList(),
createBaseSkill(
sheet = sheet,
id = CharacterSheet.CommonSkillId.THROW_ID,
label = getString(Res.string.character_sheet_edit__skills__throw),
base = derivedStateOf { normalize(str() + dex()) },
),
SkillGroup(
type = SkillGroup.Type.MAGICS,
title = getString(Res.string.character_sheet_edit__magic__title),
action = getString(Res.string.character_sheet_edit__magic__add_action),
fields = sheet?.magics?.map {
FieldUio.create(
initialLabel = it.label,
initialValue = it.value.toString()
)
} ?: emptyList(),
createBaseSkill(
sheet = sheet,
id = CharacterSheet.CommonSkillId.ATHLETICS_ID,
label = getString(Res.string.character_sheet_edit__skills__athletics),
base = derivedStateOf { normalize(str() + con() * 2) },
),
createBaseSkill(
sheet = sheet,
id = CharacterSheet.CommonSkillId.ACROBATICS_ID,
label = getString(Res.string.character_sheet_edit__skills__acrobatics),
base = derivedStateOf { normalize(dex() + con() * 2) },
),
createBaseSkill(
sheet = sheet,
id = CharacterSheet.CommonSkillId.PERCEPTION_ID,
label = getString(Res.string.character_sheet_edit__skills__perception),
base = derivedStateOf { normalize(10 + int() * 2) },
),
createBaseSkill(
sheet = sheet,
id = CharacterSheet.CommonSkillId.SEARCH_ID,
label = getString(Res.string.character_sheet_edit__skills__search),
base = derivedStateOf { normalize(10 + int() * 2) },
),
createBaseSkill(
sheet = sheet,
id = CharacterSheet.CommonSkillId.EMPATHY_ID,
label = getString(Res.string.character_sheet_edit__skills__empathy),
base = derivedStateOf { normalize(cha() + int()) },
),
createBaseSkill(
sheet = sheet,
id = CharacterSheet.CommonSkillId.PERSUASION_ID,
label = getString(Res.string.character_sheet_edit__skills__persuasion),
base = derivedStateOf { normalize(cha() * 3) },
),
createBaseSkill(
sheet = sheet,
id = CharacterSheet.CommonSkillId.INTIMIDATION_ID,
label = getString(Res.string.character_sheet_edit__skills__intimidation),
base = derivedStateOf { normalize(cha() + max(pow(), hei()) * 2) },
),
createBaseSkill(
sheet = sheet,
id = CharacterSheet.CommonSkillId.SPIEL_ID,
label = getString(Res.string.character_sheet_edit__skills__spiel),
base = derivedStateOf { normalize(cha() * 2 + int()) },
),
createBaseSkill(
sheet = sheet,
id = CharacterSheet.CommonSkillId.BARGAIN_ID,
label = getString(Res.string.character_sheet_edit__skills__bargain),
base = derivedStateOf { normalize(cha() * 2) },
),
createBaseSkill(
sheet = sheet,
id = CharacterSheet.CommonSkillId.DISCRETION_ID,
label = getString(Res.string.character_sheet_edit__skills__discretion),
base = derivedStateOf { normalize(cha() + dex() * 2 - hei()) },
),
createBaseSkill(
sheet = sheet,
id = CharacterSheet.CommonSkillId.SLEIGHT_OF_HAND_ID,
label = getString(Res.string.character_sheet_edit__skills__sleight_of_hand),
base = derivedStateOf { normalize(dex() * 2) },
),
createBaseSkill(
sheet = sheet,
id = CharacterSheet.CommonSkillId.AID_ID,
label = getString(Res.string.character_sheet_edit__skills__aid),
base = derivedStateOf { normalize(int() + dex()) },
),
),
rolls = sheet?.rolls?.map {
FieldUio.create(
isLabelEditable = true,
initialLabel = it.label,
initialValue = it.roll,
specialSkills = sheet?.specialSkills?.map { skill ->
skillFactory.createSkill(
id = skill.id,
label = specialSkillsLabel,
labelValue = skill.label,
baseValue = parser.convertInstructionToString(instructions = skill.base),
bonusValue = skill.bonus.takeIf { it > 0 }?.toString() ?: "",
levelValue = skill.level.takeIf { it > 0 }?.toString() ?: "",
options = run {
val current = sheet.specialSkills.firstOrNull { it.id == skill.id }
listOf(
skillFactory.occupationOption(checked = current?.occupation ?: false),
skillFactory.deleteOption { onDeleteSkill(skill.id) },
)
},
)
} ?: emptyList()
} ?: emptyList(),
magicSkills = sheet?.magicSkills?.map { skill ->
skillFactory.createSkill(
id = skill.id,
label = magicSkillsLabel,
labelValue = skill.label,
baseValue = parser.convertInstructionToString(instructions = skill.base),
bonusValue = skill.bonus.takeIf { it > 0 }?.toString() ?: "",
levelValue = skill.level.takeIf { it > 0 }?.toString() ?: "",
options = run {
val current = sheet.magicSkills.firstOrNull { it.id == skill.id }
listOf(
skillFactory.occupationOption(checked = current?.occupation ?: false),
skillFactory.deleteOption { onDeleteSkill(skill.id) },
)
},
)
} ?: emptyList(),
actions = emptyList(),
)
}
private suspend fun createBaseSkill(
sheet: CharacterSheet?,
id: String,
label: String,
base: State<Int>,
): BaseSkillFieldUio {
val skill = sheet?.commonSkills?.firstOrNull { it.id == id }
return BaseSkillFieldUio(
id = id,
label = label,
base = base,
bonus = skillFactory.createWrapper(
label = getString(Res.string.character_sheet_edit__skills__bonus_label),
value = skill?.bonus?.takeIf { it > 0 }?.toString() ?: "",
),
level = skillFactory.createWrapper(
label = getString(Res.string.character_sheet_edit__skills__level_label),
value = skill?.level?.takeIf { it > 0 }?.toString() ?: "",
),
option = skillFactory.occupationOption(skill?.occupation ?: false),
)
}
private inline fun <reified T> FieldUio.unpack(): T {
val tmp = value.value.ifBlank { valuePlaceHolder.value }
return when (T::class) {

View file

@ -21,6 +21,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Add
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
@ -32,44 +33,70 @@ import com.pixelized.desktop.lwa.LocalWindowController
import com.pixelized.desktop.lwa.composable.decoratedBox.DecoratedBox
import com.pixelized.desktop.lwa.navigation.screen.LocalScreenController
import com.pixelized.desktop.lwa.navigation.window.LocalWindow
import com.pixelized.desktop.lwa.navigation.window.destination.CharacterSheetCreateWindow
import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.BaseSkillFieldUio
import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.BaseSkillForm
import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.FieldUio
import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.Form
import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.SkillFieldUio
import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.SkillForm
import kotlinx.coroutines.launch
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.character_sheet__skills__common_title
import lwacharactersheet.composeapp.generated.resources.character_sheet__skills__magic_title
import lwacharactersheet.composeapp.generated.resources.character_sheet__skills__special_title
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__add_roll_action
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__characteristics__title
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__save_action
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__magic_action
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__special_action
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__sub_characteristics__title
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__title
import org.jetbrains.compose.resources.stringResource
import org.koin.compose.viewmodel.koinViewModel
import org.koin.core.annotation.KoinExperimentalAPI
@Stable
data class CharacterSheetEditPageUio(
val id: String,
val name: FieldUio,
val skills: List<SkillGroup>,
val rolls: List<FieldUio>,
val strength: FieldUio,
val dexterity: FieldUio,
val constitution: FieldUio,
val height: FieldUio,
val intelligence: FieldUio,
val power: FieldUio,
val charisma: FieldUio,
val movement: FieldUio,
val maxHp: FieldUio,
val maxPP: FieldUio,
val damageBonus: FieldUio,
val armor: FieldUio,
val commonSkills: List<BaseSkillFieldUio>,
val specialSkills: List<SkillFieldUio>,
val magicSkills: List<SkillFieldUio>,
val actions: List<FieldUio>,
) {
@Stable
data class SkillGroup(
val title: String,
val type: Type,
val action: String? = null,
val fields: List<FieldUio>,
) {
@Stable
enum class Type {
CHARACTERISTICS,
SUB_CHARACTERISTICS,
SKILLS,
OCCUPATIONS,
MAGICS,
OTHERS,
}
}
val characteristics
get() = listOf(
strength,
dexterity,
constitution,
height,
intelligence,
power,
charisma,
)
val subCharacteristics
get() = listOf(
movement,
maxHp,
maxPP,
damageBonus,
armor,
)
}
@OptIn(KoinExperimentalAPI::class)
@Composable
fun CharacterSheetEditPage(
viewModel: CharacterSheetEditViewModel = koinViewModel(),
@ -79,6 +106,10 @@ fun CharacterSheetEditPage(
val screen = LocalScreenController.current
val scope = rememberCoroutineScope()
LaunchedEffect(Unit) {
window.resize(size = CharacterSheetCreateWindow.size)
}
Surface(
modifier = Modifier.fillMaxSize(),
) {
@ -91,8 +122,21 @@ fun CharacterSheetEditPage(
null
}
},
onNewSkill = viewModel::onSkill,
onNewCategory = viewModel::onNewRoll,
onNewSpecialSkill = {
scope.launch {
viewModel.onNewSpecialSkill()
}
},
onNewMagicSkill = {
scope.launch {
viewModel.onNewMagicSkill()
}
},
onNewAction = {
scope.launch {
viewModel.onNewAction()
}
},
onSave = {
scope.launch {
viewModel.save()
@ -109,8 +153,9 @@ fun CharacterSheetEditPage(
fun CharacterSheetEdit(
form: CharacterSheetEditPageUio,
onBack: (() -> Unit)?,
onNewSkill: (CharacterSheetEditPageUio.SkillGroup) -> Unit,
onNewCategory: () -> Unit,
onNewSpecialSkill: () -> Unit,
onNewMagicSkill: () -> Unit,
onNewAction: () -> Unit,
onSave: () -> Unit,
) {
Scaffold(
@ -142,7 +187,7 @@ fun CharacterSheetEdit(
modifier = Modifier
.verticalScroll(state = rememberScrollState())
.padding(paddingValues = paddingValues)
.padding(all = 24.dp),
.padding(all = 16.dp),
verticalArrangement = Arrangement.spacedBy(space = 16.dp)
) {
Form(
@ -150,56 +195,161 @@ fun CharacterSheetEdit(
field = form.name,
)
form.skills.forEach {
DecoratedBox(
modifier = Modifier.animateContentSize(),
DecoratedBox(
modifier = Modifier.animateContentSize(),
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
modifier = Modifier.padding(vertical = 8.dp),
style = MaterialTheme.typography.caption,
text = it.title,
Text(
modifier = Modifier.padding(vertical = 8.dp),
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = MaterialTheme.typography.caption,
text = stringResource(Res.string.character_sheet_edit__characteristics__title),
)
form.characteristics.forEach {
Form(
modifier = Modifier.fillMaxWidth(),
field = it,
)
it.fields.forEach {
Form(
modifier = Modifier.fillMaxWidth(),
field = it,
}
}
}
DecoratedBox(
modifier = Modifier.animateContentSize(),
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
modifier = Modifier.padding(vertical = 8.dp),
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = MaterialTheme.typography.caption,
text = stringResource(Res.string.character_sheet_edit__sub_characteristics__title),
)
form.subCharacteristics.forEach {
Form(
modifier = Modifier.fillMaxWidth(),
field = it,
)
}
}
}
DecoratedBox(
modifier = Modifier.animateContentSize(),
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
modifier = Modifier.padding(vertical = 8.dp),
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = MaterialTheme.typography.caption,
text = stringResource(Res.string.character_sheet__skills__common_title),
)
form.commonSkills.forEach {
BaseSkillForm(
modifier = Modifier
.fillMaxWidth()
.padding(end = 4.dp),
field = it,
)
}
}
}
DecoratedBox(
modifier = Modifier.animateContentSize(),
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
modifier = Modifier.padding(vertical = 8.dp),
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = MaterialTheme.typography.caption,
text = stringResource(Res.string.character_sheet__skills__special_title),
)
form.specialSkills.forEach {
SkillForm(
modifier = Modifier
.fillMaxWidth()
.padding(end = 4.dp),
field = it,
)
}
TextButton(
modifier = Modifier.align(alignment = Alignment.End),
onClick = onNewSpecialSkill,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
) {
Text(
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = stringResource(Res.string.character_sheet_edit__skills__special_action),
)
Icon(
imageVector = Icons.Default.Add,
contentDescription = null,
)
}
it.action?.let { label ->
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(
space = 8.dp,
alignment = Alignment.End
)
) {
TextButton(
onClick = { onNewSkill(it) },
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
) {
Text(
text = label,
)
Icon(
imageVector = Icons.Default.Add,
contentDescription = null,
)
}
}
}
}
}
}
}
form.rolls.forEach {
DecoratedBox(
modifier = Modifier.animateContentSize(),
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
modifier = Modifier.padding(vertical = 8.dp),
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = MaterialTheme.typography.caption,
text = stringResource(Res.string.character_sheet__skills__magic_title),
)
form.magicSkills.forEach {
SkillForm(
modifier = Modifier
.fillMaxWidth()
.padding(end = 4.dp),
field = it,
)
}
TextButton(
modifier = Modifier.align(alignment = Alignment.End),
onClick = onNewMagicSkill,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
) {
Text(
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = stringResource(Res.string.character_sheet_edit__skills__magic_action),
)
Icon(
imageVector = Icons.Default.Add,
contentDescription = null,
)
}
}
}
}
form.actions.forEach {
Form(
modifier = Modifier.fillMaxWidth(),
valueWidth = 120.dp,
@ -212,9 +362,13 @@ fun CharacterSheetEdit(
horizontalArrangement = Arrangement.End,
) {
TextButton(
onClick = onNewCategory,
onClick = onNewAction,
) {
Text(text = stringResource(Res.string.character_sheet_edit__add_roll_action))
Text(
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = stringResource(Res.string.character_sheet_edit__add_roll_action)
)
}
}
@ -225,7 +379,11 @@ fun CharacterSheetEdit(
TextButton(
onClick = onSave,
) {
Text(text = stringResource(Res.string.character_sheet_edit__save_action))
Text(
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = stringResource(Res.string.character_sheet_edit__save_action),
)
}
}
}

View file

@ -6,78 +6,106 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import com.pixelized.desktop.lwa.navigation.screen.destination.CharacterSheetEditDestination
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.desktop.lwa.screen.characterSheet.edit.CharacterSheetEditPageUio.SkillGroup
import com.pixelized.desktop.lwa.screen.characterSheet.common.SkillFieldFactory
import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.FieldUio
import kotlinx.coroutines.runBlocking
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__magic_title
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__special_title
import org.jetbrains.compose.resources.getString
import java.util.UUID
class CharacterSheetEditViewModel(
private val characterSheetRepository: CharacterSheetRepository,
private val sheetFactory: CharacterSheetEditFactory,
private val skillFactory: SkillFieldFactory,
savedStateHandle: SavedStateHandle,
) : ViewModel() {
private val argument = CharacterSheetEditDestination.Argument(savedStateHandle)
private val factory = CharacterSheetEditFactory()
val enableBack = argument.enableBack
private val _characterSheet = mutableStateOf(
characterSheetRepository.characterSheetFlow(id = argument.id).value.let {
runBlocking { factory.convertToUio(it) }
runBlocking {
sheetFactory.convertToUio(
sheet = it,
onDeleteSkill = ::deleteSkill,
)
}
}
)
val characterSheet: State<CharacterSheetEditPageUio> get() = _characterSheet
fun onSkill(skill: SkillGroup) {
val sheet = _characterSheet.value
_characterSheet.value = sheet.copy(
skills = sheet.skills.map { group ->
if (skill.title == group.title) {
group.copy(
fields = mutableListOf<FieldUio>().apply {
addAll(group.fields)
add(
FieldUio.create(
isLabelEditable = true,
initialLabel = "",
valuePlaceHolder = {
when (group.type) {
SkillGroup.Type.CHARACTERISTICS -> ""
SkillGroup.Type.SUB_CHARACTERISTICS -> ""
SkillGroup.Type.SKILLS -> "0"
SkillGroup.Type.OCCUPATIONS -> "40"
SkillGroup.Type.MAGICS -> "0"
SkillGroup.Type.OTHERS -> ""
}
},
)
)
}
)
} else {
group
}
}
suspend fun onNewSpecialSkill() {
val id = UUID.randomUUID().toString()
val skill = skillFactory.createSkill(
id = id,
label = getString(Res.string.character_sheet_edit__skills__special_title),
options = listOf(
skillFactory.occupationOption(checked = false),
skillFactory.deleteOption { deleteSkill(skillId = id) }
),
)
val skills = _characterSheet.value.specialSkills
.toMutableList()
.also { it.add(skill) }
_characterSheet.value = characterSheet.value.copy(
specialSkills = skills,
)
}
fun onNewRoll() {
val sheet = _characterSheet.value
_characterSheet.value = sheet.copy(
rolls = sheet.rolls.toMutableList().apply {
add(
FieldUio.create(
initialLabel = "",
isLabelEditable = true,
valuePlaceHolder = { "" },
)
)
}
suspend fun onNewMagicSkill() {
val id = UUID.randomUUID().toString()
val skill = skillFactory.createSkill(
id = id,
label = getString(Res.string.character_sheet_edit__skills__magic_title),
options = listOf(
skillFactory.occupationOption(checked = false),
skillFactory.deleteOption { deleteSkill(skillId = id) }
),
)
val skills = _characterSheet.value.magicSkills
.toMutableList()
.also { it.add(skill) }
_characterSheet.value = characterSheet.value.copy(
magicSkills = skills,
)
}
suspend fun onNewAction() {
val field = FieldUio.create(
initialLabel = "",
isLabelEditable = true,
valuePlaceHolder = { "" },
)
val actions = _characterSheet.value.actions.toMutableList().also {
it.add(field)
}
_characterSheet.value = _characterSheet.value.copy(
actions = actions,
)
}
private fun deleteSkill(skillId: String) {
_characterSheet.value = _characterSheet.value.copy(
specialSkills = _characterSheet.value.specialSkills.toMutableList().also { skills ->
skills.removeIf { it.id == skillId }
},
magicSkills = _characterSheet.value.magicSkills.toMutableList().also { skills ->
skills.removeIf { it.id == skillId }
},
)
}
suspend fun save() {
val sheet = _characterSheet.value
val model = factory.convertToModel(sheet = sheet)
characterSheetRepository.save(characterSheet = model)
val updatedSheet = sheetFactory.updateCharacterSheet(
currentSheet = characterSheetRepository.characterSheetFlow(id = _characterSheet.value.id).value,
editedSheet = _characterSheet.value,
)
characterSheetRepository.save(
characterSheet = updatedSheet,
)
}
}

View file

@ -0,0 +1,117 @@
package com.pixelized.desktop.lwa.screen.characterSheet.edit.composable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.ContentAlpha
import androidx.compose.material.DropdownMenu
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.option.CheckedOption
import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.option.DropDownCheckedMenuItem
import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.textfield.TextFieldWrapper
import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.textfield.TextFieldWrapperUio
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__base_label
import org.jetbrains.compose.resources.stringResource
@Stable
class BaseSkillFieldUio(
val id: String,
val label: String,
val base: State<Int>,
val bonus: TextFieldWrapperUio,
val level: TextFieldWrapperUio,
val option: CheckedOption,
)
@Composable
fun BaseSkillForm(
modifier: Modifier = Modifier,
field: BaseSkillFieldUio,
) {
val showMenu = remember { mutableStateOf(false) }
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(space = 4.dp, alignment = Alignment.End),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier
.weight(1f)
.padding(start = 16.dp, bottom = 12.dp)
.align(alignment = Alignment.Bottom),
style = MaterialTheme.typography.body1,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = field.label,
)
Column(
modifier = Modifier
.width(width = 96.dp)
.padding(start = 16.dp),
) {
Text(
style = MaterialTheme.typography.caption,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
color = MaterialTheme.colors.onSurface.copy(ContentAlpha.medium),
text = stringResource(Res.string.character_sheet_edit__skills__base_label),
)
Text(
style = MaterialTheme.typography.body1,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = "+${field.base.value}",
)
}
TextFieldWrapper(
modifier = Modifier.width(width = 96.dp),
wrapper = field.bonus,
)
TextFieldWrapper(
modifier = Modifier.width(width = 96.dp),
wrapper = field.level,
)
Box {
IconButton(
onClick = { showMenu.value = showMenu.value.not() },
) {
Icon(
imageVector = Icons.Default.MoreVert,
tint = MaterialTheme.colors.primary,
contentDescription = null,
)
}
DropdownMenu(
expanded = showMenu.value,
onDismissRequest = { showMenu.value = false }
) {
DropDownCheckedMenuItem(
wrapper = field.option,
onClick = {
showMenu.value = false
field.option.onOption()
}
)
}
}
}
}

View file

@ -4,7 +4,9 @@ import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
@ -24,7 +26,9 @@ import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.utils.preview.ContentPreview
@Deprecated("redo")
@Stable
open class FieldUio(
val isLabelDisplayed: Boolean,
@ -127,4 +131,23 @@ fun Form(
}
}
}
}
@Composable
@Preview
private fun Preview() {
ContentPreview(
paddingValues = PaddingValues(all = 16.dp)
) {
Form(
field = FieldUio.create(
isLabelDisplayed = true,
isLabelEditable = true,
initialLabel = "label",
labelPlaceHolder = { "labelPlaceholder" },
initialValue = "value",
valuePlaceHolder = { "valuePlaceholder" },
)
)
}
}

View file

@ -0,0 +1,257 @@
package com.pixelized.desktop.lwa.screen.characterSheet.edit.composable
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.width
import androidx.compose.material.DropdownMenu
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.singleWindowApplication
import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.textfield.TextFieldWrapperUio
import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.textfield.TextFieldWrapper
import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.option.ActionOption
import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.option.CheckedOption
import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.option.DropDownMenuItemWrapper
import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.option.OptionUio
import com.pixelized.desktop.lwa.utils.preview.ContentPreview
import java.util.UUID
@Stable
class SkillFieldUio(
val id: String,
val label: TextFieldWrapperUio,
val base: TextFieldWrapperUio,
val bonus: TextFieldWrapperUio,
val level: TextFieldWrapperUio,
val options: List<OptionUio>,
)
@Composable
fun SkillForm(
modifier: Modifier = Modifier,
field: SkillFieldUio,
) {
val showMenu = remember { mutableStateOf(false) }
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(space = 4.dp, alignment = Alignment.End),
verticalAlignment = Alignment.CenterVertically,
) {
TextFieldWrapper(
modifier = Modifier.weight(1f),
wrapper = field.label,
)
TextFieldWrapper(
modifier = Modifier.width(width = 96.dp),
wrapper = field.base,
)
TextFieldWrapper(
modifier = Modifier.width(width = 96.dp),
wrapper = field.bonus,
)
TextFieldWrapper(
modifier = Modifier.width(width = 96.dp),
wrapper = field.level,
)
if (field.options.isNotEmpty()) {
Box {
IconButton(
onClick = { showMenu.value = showMenu.value.not() },
) {
Icon(
imageVector = Icons.Default.MoreVert,
tint = MaterialTheme.colors.primary,
contentDescription = null,
)
}
DropdownMenu(
expanded = showMenu.value,
onDismissRequest = { showMenu.value = false }
) {
field.options.forEach {
DropDownMenuItemWrapper(
wrapper = it,
onClick = {
showMenu.value = false
it.onOption()
}
)
}
}
}
}
}
}
// PREVIEW
@Composable
@Preview
private fun Preview() {
ContentPreview(
modifier = Modifier.width(width = 1000.dp),
paddingValues = PaddingValues(all = 16.dp),
) {
SkillForm(
field = skillFieldUio(
labelLabel = "compétence",
labelValue = "Bagarre",
baseLabel = "Base",
baseValue = "Stat(DEX*2)",
bonusLabel = "Bonus",
bonusValue = "50",
levelLabel = "Niveau",
levelValue = "5",
),
)
}
}
fun main() = singleWindowApplication(title = "Context menu") {
ContentPreview(
modifier = Modifier.width(1000.dp),
paddingValues = PaddingValues(all = 16.dp),
) {
Column(
verticalArrangement = Arrangement.spacedBy(space = 8.dp)
) {
SkillForm(
field = skillFieldUio(
labelLabel = "compétence",
labelValue = "Bagarre",
baseLabel = "Base",
baseValue = "Stat(DEX*2)",
bonusLabel = "Bonus",
bonusValue = "55",
levelLabel = "Niveau",
levelValue = "0",
options = listOf(
CheckedOption.OccupationOption(
label = "Base minimum 40",
checked = mutableStateOf(true),
onOption = { },
),
ActionOption.DeleteOptionUio(
icon = Icons.Default.Delete,
label = "Supprimer",
onOption = { },
),
)
),
)
SkillForm(
field = skillFieldUio(
labelLabel = "compétence",
labelValue = "Esquive",
baseLabel = "Base",
baseValue = "Stat(DEX*2)",
bonusLabel = "Bonus",
bonusValue = "40",
levelLabel = "Niveau",
levelValue = "0",
options = listOf(
CheckedOption.OccupationOption(
label = "Base minimum 40",
checked = mutableStateOf(true),
onOption = { },
),
ActionOption.DeleteOptionUio(
icon = Icons.Default.Delete,
label = "Supprimer",
onOption = { },
),
)
),
)
SkillForm(
field = skillFieldUio(
labelLabel = "compétence",
labelValue = "Saisie",
baseLabel = "Base",
baseValue = "Stat(FOR+TAI)",
bonusLabel = "Bonus",
bonusValue = "0",
levelLabel = "Niveau",
levelValue = "0",
options = listOf(
CheckedOption.OccupationOption(
label = "Base minimum 40",
checked = mutableStateOf(true),
onOption = { },
),
ActionOption.DeleteOptionUio(
icon = Icons.Default.Delete,
label = "Supprimer",
onOption = { },
),
)
),
)
}
}
}
private fun skillFieldUio(
labelLabel: String,
labelValue: String,
baseLabel: String,
baseValue: String,
bonusLabel: String,
bonusValue: String,
levelLabel: String,
levelValue: String,
options: List<OptionUio> = emptyList(),
): SkillFieldUio {
return SkillFieldUio(
id = UUID.randomUUID().toString(),
label = mutableStateOf(labelValue).let { state ->
TextFieldWrapperUio(
enable = true,
label = labelLabel,
value = state,
onValueChange = { state.value = it },
)
},
base = mutableStateOf(baseValue).let { state ->
TextFieldWrapperUio(
enable = true,
label = baseLabel,
value = state,
onValueChange = { state.value = it },
)
},
bonus = mutableStateOf(bonusValue).let { state ->
TextFieldWrapperUio(
enable = true,
label = bonusLabel,
value = state,
onValueChange = { state.value = it },
)
},
level = mutableStateOf(levelValue).let { state ->
TextFieldWrapperUio(
enable = true,
label = levelLabel,
value = state,
onValueChange = { state.value = it },
)
},
options = options,
)
}

View file

@ -0,0 +1,45 @@
package com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.option
import androidx.compose.foundation.layout.padding
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
@Stable
sealed class ActionOption(
val icon: ImageVector,
val label: String,
onOption: () -> Unit,
) : OptionUio(onOption = onOption) {
class DeleteOptionUio(icon: ImageVector, label: String, onOption: () -> Unit) :
ActionOption(icon = icon, label = label, onOption = onOption)
}
@Composable
fun DropDownActionMenuItem(
modifier: Modifier = Modifier,
wrapper: ActionOption,
onClick: () -> Unit,
) {
DropdownMenuItem(
modifier = modifier,
onClick = onClick,
) {
Icon(
tint = MaterialTheme.colors.primary,
imageVector = wrapper.icon,
contentDescription = null,
)
Text(
modifier = Modifier.padding(start = 8.dp),
text = wrapper.label,
)
}
}

View file

@ -0,0 +1,49 @@
package com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.option
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.Checkbox
import androidx.compose.material.CheckboxDefaults
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Stable
sealed class CheckedOption(
val label: String,
val checked: State<Boolean>,
onOption: () -> Unit,
) : OptionUio(onOption = onOption) {
class OccupationOption(label: String, checked: State<Boolean>, onOption: () -> Unit) :
CheckedOption(label = label, checked = checked, onOption = onOption)
}
@Composable
fun DropDownCheckedMenuItem(
modifier: Modifier = Modifier,
wrapper: CheckedOption,
onClick: () -> Unit,
) {
DropdownMenuItem(
modifier = modifier,
onClick = onClick,
) {
Checkbox(
modifier = Modifier.size(size = 24.dp),
checked = wrapper.checked.value,
colors = CheckboxDefaults.colors(
checkedColor = MaterialTheme.colors.primary,
),
onCheckedChange = null
)
Text(
modifier = Modifier.padding(start = 8.dp),
text = wrapper.label,
)
}
}

View file

@ -0,0 +1,31 @@
package com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.option
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier
@Stable
sealed class OptionUio(
val onOption: () -> Unit,
)
@Composable
fun DropDownMenuItemWrapper(
modifier: Modifier = Modifier,
wrapper: OptionUio,
onClick: () -> Unit,
) {
when (wrapper) {
is ActionOption -> DropDownActionMenuItem(
modifier = modifier,
wrapper = wrapper,
onClick = onClick,
)
is CheckedOption -> DropDownCheckedMenuItem(
modifier = modifier,
wrapper = wrapper,
onClick = onClick,
)
}
}

View file

@ -0,0 +1,51 @@
package com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.textfield
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.material.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.style.TextOverflow
import com.pixelized.desktop.lwa.utils.rememberKeyboardActions
@Stable
data class TextFieldWrapperUio(
val enable: Boolean,
val label: String,
val value: State<String>,
val onValueChange: (String) -> Unit,
)
@Composable
fun TextFieldWrapper(
modifier: Modifier = Modifier,
wrapper: TextFieldWrapperUio,
) {
val focus = LocalFocusManager.current
TextField(
modifier = modifier,
colors = TextFieldDefaults.textFieldColors(
backgroundColor = MaterialTheme.colors.onSurface.copy(alpha = 0.03f),
),
keyboardActions = rememberKeyboardActions {
focus.moveFocus(FocusDirection.Next)
},
enabled = wrapper.enable,
singleLine = true,
label = {
Text(
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = wrapper.label
)
},
onValueChange = { wrapper.onValueChange(it) },
value = wrapper.value.value,
)
}

View file

@ -31,7 +31,6 @@ import lwacharactersheet.composeapp.generated.resources.main_page__network_actio
import lwacharactersheet.composeapp.generated.resources.main_page__roll_history_action
import org.jetbrains.compose.resources.stringResource
import org.koin.compose.viewmodel.koinViewModel
import org.koin.core.annotation.KoinExperimentalAPI
@Stable
data class CharacterUio(
@ -39,7 +38,6 @@ data class CharacterUio(
val name: String,
)
@OptIn(KoinExperimentalAPI::class)
@Composable
fun MainPage(
viewModel: MainPageViewModel = koinViewModel(),
@ -54,7 +52,7 @@ fun MainPage(
modifier = Modifier
.verticalScroll(state = rememberScrollState())
.fillMaxSize()
.padding(horizontal = 24.dp),
.padding(horizontal = 16.dp),
contentAlignment = Alignment.Center,
) {
MainPageContent(

View file

@ -44,7 +44,6 @@ import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.pixelized.desktop.lwa.screen.roll.DifficultyUio.Difficulty
import com.pixelized.desktop.lwa.utils.DisableInteractionSource
import kotlinx.coroutines.launch
@ -60,7 +59,6 @@ import lwacharactersheet.composeapp.generated.resources.roll_page__roll__success
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource
import org.koin.compose.viewmodel.koinViewModel
import org.koin.core.annotation.KoinExperimentalAPI
@Stable
data class RollTitleUio(
@ -84,7 +82,6 @@ data class DifficultyUio(
}
}
@OptIn(KoinExperimentalAPI::class)
@Composable
fun RollPage(
viewModel: RollViewModel = koinViewModel(),
@ -131,6 +128,10 @@ fun RollPage(
horizontalAlignment = Alignment.CenterHorizontally,
) {
Box(
modifier = Modifier.graphicsLayer {
this.scaleX = viewModel.rollScale.value
this.scaleY = viewModel.rollScale.value
},
contentAlignment = Alignment.Center
) {
Icon(
@ -150,8 +151,8 @@ fun RollPage(
AnimatedContent(
targetState = viewModel.rollResult.value?.value?.toString() ?: "",
transitionSpec = {
val enter = fadeIn() + slideInVertically { 32 }
val exit = fadeOut() + slideOutVertically { -32 }
val enter = fadeIn() + slideInVertically { -32 }
val exit = fadeOut() + slideOutVertically { 32 }
enter togetherWith exit using SizeTransform(clip = false)
},
) { label ->

View file

@ -3,6 +3,7 @@ package com.pixelized.desktop.lwa.screen.roll
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
@ -48,6 +49,7 @@ class RollViewModel(
val rollResult: State<RollResultUio?> get() = _rollResult
val rollRotation = Animatable(0f)
val rollScale = Animatable(1f)
private val _rollDifficulty = mutableStateOf<DifficultyUio?>(null)
val rollDifficulty: State<DifficultyUio?> get() = _rollDifficulty
@ -95,7 +97,10 @@ class RollViewModel(
rollAction: String,
rollSuccessValue: Int?,
) {
runBlocking { rollRotation.snapTo(0f) }
runBlocking {
rollRotation.snapTo(0f)
rollScale.snapTo(1f)
}
this.sheet = characterSheetRepository.characterSheetFlow(id = sheet.id).value!!
this.rollAction = rollAction
@ -124,6 +129,22 @@ class RollViewModel(
rollJob?.cancel()
rollJob = launch {
launch {
rollScale.animateTo(
targetValue = 1.20f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioNoBouncy,
stiffness = 800f,
)
)
rollScale.animateTo(
targetValue = 1f,
animationSpec = spring(
dampingRatio = 0.28f,
stiffness = 800f,
)
)
}
launch {
rollRotation.animateTo(
targetValue = rollRotation.value.let { it - it % 360 } + 360f * 3,

View file

@ -0,0 +1,31 @@
package com.pixelized.desktop.lwa.utils.preview
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.pixelized.desktop.lwa.theme.LwaTheme
@Composable
fun ContentPreview(
modifier: Modifier = Modifier,
paddingValues: PaddingValues = PaddingValues(),
content: @Composable () -> Unit,
) {
LwaTheme {
Surface {
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues = paddingValues)
.then(other = modifier),
contentAlignment = Alignment.Center,
content = { content() },
)
}
}
}

View file

@ -0,0 +1,9 @@
package com.pixelized.desktop.lwa.utils
import androidx.compose.foundation.text.KeyboardActionScope
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
@Composable
fun rememberKeyboardActions(onAny: KeyboardActionScope.() -> Unit) = remember { KeyboardActions(onAny = onAny) }

View file

@ -0,0 +1,156 @@
package com.pixelized.desktop.lwa.parser
import com.pixelized.desktop.lwa.parser.arithmetic.Arithmetic
import com.pixelized.desktop.lwa.parser.arithmetic.ArithmeticParser
import com.pixelized.desktop.lwa.parser.arithmetic.Instruction
import kotlin.test.Test
import kotlin.test.assertFails
class ArithmeticParserTest {
@Test
fun testDiceInstructionParse() {
val parser = ArithmeticParser()
fun test(
instruction: String,
expectedModifier: Instruction.Dice.Modifier?,
expectedQuantity: Int,
expectedFaces: Int,
) {
val dice = parser.parseInstruction(instruction = instruction)
assert(dice is Instruction.Dice) {
"Instruction should be ArithmeticInstruction.Dice but was: ${dice::class.java.simpleName}"
}
(dice as? Instruction.Dice)?.let {
assert(dice.modifier == expectedModifier) {
"$instruction modifier should be:\"$expectedModifier\", but was: ${dice.modifier}"
}
assert(dice.quantity == expectedQuantity) {
"$instruction quantity should be \"$expectedQuantity\" but was ${dice.quantity}"
}
assert(dice.faces == expectedFaces) {
"$instruction faces should be \"$expectedFaces\" but was ${dice.faces}"
}
}
}
test(
instruction = "1d100",
expectedModifier = null,
expectedQuantity = 1,
expectedFaces = 100,
)
test(
instruction = "a2d6",
expectedModifier = Instruction.Dice.Modifier.ADVANTAGE,
expectedQuantity = 2,
expectedFaces = 6,
)
test(
instruction = "d1d2",
expectedModifier = Instruction.Dice.Modifier.DISADVANTAGE,
expectedQuantity = 1,
expectedFaces = 2,
)
test(
instruction = "e6d6",
expectedModifier = Instruction.Dice.Modifier.EMPHASIS,
expectedQuantity = 6,
expectedFaces = 6,
)
}
@Test
fun testWordInstructionParse() {
val parser = ArithmeticParser()
ArithmeticParser.words.map { instruction ->
val word = parser.parseInstruction(instruction = instruction)
assert(word is Instruction.Word) {
"Instruction should be ArithmeticInstruction.Word but was: ${word::class.java.simpleName}"
}
(word as? Instruction.Word)?.let {
assert(it.name == instruction) {
"Instruction should be $instruction, but was ${it.name}"
}
}
}
}
@Test
fun testFlatInstructionParse() {
val parser = ArithmeticParser()
"100".let { instruction ->
val flat = parser.parseInstruction(instruction = instruction)
assert(flat is Instruction.Flat) {
"Instruction should be ArithmeticInstruction.Flat but was: ${flat::class.java.simpleName}"
}
(flat as? Instruction.Flat)?.let {
assert("${it.value}" == instruction) {
"Instruction should be $instruction, but was ${it.value}"
}
}
}
}
@Test
fun testFailedInstructionParse() {
val parser = ArithmeticParser()
assertFails(
message = "Instruction parse should failed with UnknownInstruction",
) {
parser.parseInstruction(instruction = "a110")
}
}
@Test
fun testRollParse() {
val parser = ArithmeticParser()
fun test(
arithmetics: Arithmetic,
expectedSign: Int,
expectedInstruction: Instruction,
) {
assert(arithmetics.sign == expectedSign) {
"Arithmetic sign should be $expectedSign but was: ${arithmetics.sign}"
}
assert(arithmetics.instruction == expectedInstruction) {
"Arithmetic instruction should be $expectedInstruction but was: ${arithmetics.instruction}"
}
}
val instructions = parser.parse(value = "1+1d6+2-BDC+BDD")
test(
arithmetics = instructions[0],
expectedSign = 1,
expectedInstruction = Instruction.Flat(value = 1),
)
test(
arithmetics = instructions[1],
expectedSign = 1,
expectedInstruction = Instruction.Dice(quantity = 1, faces = 6, modifier = null),
)
test(
arithmetics = instructions[2],
expectedSign = 1,
expectedInstruction = Instruction.Flat(value = 2),
)
test(
arithmetics = instructions[3],
expectedSign = -1,
expectedInstruction = Instruction.Word.BDC,
)
test(
arithmetics = instructions[4],
expectedSign = 1,
expectedInstruction = Instruction.Word.BDD,
)
}
}

View file

@ -15,9 +15,12 @@ composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "k
kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
[libraries]
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
compose-desktop-preview = { group = "org.jetbrains.compose.ui", name = "ui-tooling-preview", version.ref = "compose-multiplatform" }
androidx-lifecycle-viewmodel = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "androidx-lifecycle" }
androidx-lifecycle-runtime-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
androidx-navigation-compose = { group = "org.jetbrains.androidx.navigation", name = "navigation-compose", version.ref = "androidx-navigation" }