diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d16df8c..450fb38 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -107,6 +107,8 @@ dependencies { implementation("androidx.compose.material3:material3:1.1.1") debugImplementation("androidx.compose.ui:ui-tooling:1.5.0") + implementation("androidx.constraintlayout:constraintlayout-compose:1.0.1") + // Navigation implementation("androidx.navigation:navigation-compose:2.7.1") diff --git a/app/src/debug/ic_launcher-playstore.png b/app/src/debug/ic_launcher-playstore.png new file mode 100644 index 0000000..e3ffc6b Binary files /dev/null and b/app/src/debug/ic_launcher-playstore.png differ diff --git a/app/src/debug/res/drawable/ic_launcher_foreground.xml b/app/src/debug/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..c27306f --- /dev/null +++ b/app/src/debug/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..7353dbd --- /dev/null +++ b/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..7353dbd --- /dev/null +++ b/app/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/debug/res/mipmap-hdpi/ic_launcher.webp b/app/src/debug/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..74016a2 Binary files /dev/null and b/app/src/debug/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/debug/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/debug/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..48e185f Binary files /dev/null and b/app/src/debug/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/debug/res/mipmap-mdpi/ic_launcher.webp b/app/src/debug/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..b29de0f Binary files /dev/null and b/app/src/debug/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/debug/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/debug/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..60210e6 Binary files /dev/null and b/app/src/debug/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/debug/res/mipmap-xhdpi/ic_launcher.webp b/app/src/debug/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..0d633d3 Binary files /dev/null and b/app/src/debug/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/debug/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/debug/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..cbb6ac3 Binary files /dev/null and b/app/src/debug/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/debug/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/debug/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..2eea795 Binary files /dev/null and b/app/src/debug/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..6eab1d3 Binary files /dev/null and b/app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..2432737 Binary files /dev/null and b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..64e523e Binary files /dev/null and b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/debug/res/values/ic_launcher_background.xml b/app/src/debug/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..0530591 --- /dev/null +++ b/app/src/debug/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #1C1B1F + \ No newline at end of file diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png index 1ec6937..689d52b 100644 Binary files a/app/src/main/ic_launcher-playstore.png and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/java/com/pixelized/rplexicon/MainActivity.kt b/app/src/main/java/com/pixelized/rplexicon/MainActivity.kt index d164a5d..58eb864 100644 --- a/app/src/main/java/com/pixelized/rplexicon/MainActivity.kt +++ b/app/src/main/java/com/pixelized/rplexicon/MainActivity.kt @@ -2,8 +2,8 @@ package com.pixelized.rplexicon import android.app.Activity import android.os.Bundle -import android.util.Log import androidx.activity.ComponentActivity +import androidx.activity.compose.BackHandler import androidx.activity.compose.setContent import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize @@ -15,13 +15,19 @@ import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.remember import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.Modifier import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.WindowCompat -import com.pixelized.rplexicon.facotry.RollParser +import androidx.hilt.navigation.compose.hiltViewModel +import com.pixelized.rplexicon.ui.composable.BlurredOverlayHost import com.pixelized.rplexicon.ui.navigation.ScreenNavHost +import com.pixelized.rplexicon.ui.screens.rolls.BlurredRollOverlayHostState +import com.pixelized.rplexicon.ui.screens.rolls.RollOverlay +import com.pixelized.rplexicon.ui.screens.rolls.RollOverlayViewModel +import com.pixelized.rplexicon.ui.screens.rolls.rememberBlurredRollOverlayHostState import com.pixelized.rplexicon.ui.theme.LexiconTheme import dagger.hilt.android.AndroidEntryPoint @@ -31,6 +37,12 @@ val LocalActivity = staticCompositionLocalOf { val LocalSnack = staticCompositionLocalOf { error("SnackbarHostState not available") } +val RollSnack = staticCompositionLocalOf { + error("SnackbarHostState not available") +} +val LocalRollOverlay = compositionLocalOf { + error("LocalRollOverlay not yet ready") +} val NO_WINDOW_INSETS = WindowInsets(0, 0, 0, 0) @AndroidEntryPoint @@ -49,9 +61,15 @@ class MainActivity : ComponentActivity() { setContent { LexiconTheme { + val rollViewModel = hiltViewModel() + val overlay = rememberBlurredRollOverlayHostState( + viewModel = rollViewModel, + ) + CompositionLocalProvider( LocalActivity provides this, - LocalSnack provides remember { SnackbarHostState() } + LocalSnack provides remember { SnackbarHostState() }, + LocalRollOverlay provides overlay, ) { Scaffold( contentWindowInsets = NO_WINDOW_INSETS, @@ -62,7 +80,17 @@ class MainActivity : ComponentActivity() { .padding(paddingValues = padding), color = MaterialTheme.colorScheme.background ) { - ScreenNavHost() + BlurredOverlayHost( + rollOverlayState = overlay, + overlay = { + RollOverlay( + viewModel = rollViewModel, + ) + }, + content = { + ScreenNavHost() + }, + ) } }, snackbarHost = { @@ -72,6 +100,9 @@ class MainActivity : ComponentActivity() { ) } ) + BackHandler(enabled = overlay.isOverlayVisible) { + overlay.hideOverlay() + } } } } diff --git a/app/src/main/java/com/pixelized/rplexicon/facotry/ConvertCharacterSheetIntoDisplayableFactory.kt b/app/src/main/java/com/pixelized/rplexicon/facotry/ConvertCharacterSheetIntoDisplayableFactory.kt new file mode 100644 index 0000000..fe0a0c5 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/facotry/ConvertCharacterSheetIntoDisplayableFactory.kt @@ -0,0 +1,186 @@ +package com.pixelized.rplexicon.facotry + +import com.pixelized.rplexicon.R +import com.pixelized.rplexicon.model.CharacterSheet +import com.pixelized.rplexicon.ui.screens.character.CharacterSheetUio +import com.pixelized.rplexicon.ui.screens.character.composable.ProficiencyUio +import com.pixelized.rplexicon.ui.screens.character.composable.SavingsThrowsUio +import com.pixelized.rplexicon.ui.screens.character.composable.StatUio +import com.pixelized.rplexicon.utilitary.extentions.modifier +import javax.inject.Inject + +class ConvertCharacterSheetIntoDisplayableFactory @Inject constructor() { + + fun toUio(sheet: CharacterSheet): CharacterSheetUio { + return CharacterSheetUio( + strength = StatUio( + label = R.string.character_sheet_stat_strength, + value = sheet.strength, + modifier = sheet.strength.modifier, + ), + dexterity = StatUio( + label = R.string.character_sheet_stat_dexterity, + value = sheet.dexterity, + modifier = sheet.dexterity.modifier, + ), + constitution = StatUio( + label = R.string.character_sheet_stat_constitution, + value = sheet.constitution, + modifier = sheet.constitution.modifier, + ), + intelligence = StatUio( + label = R.string.character_sheet_stat_intelligence, + value = sheet.intelligence, + modifier = sheet.intelligence.modifier, + ), + wisdom = StatUio( + label = R.string.character_sheet_stat_wisdom, + value = sheet.wisdom, + modifier = sheet.wisdom.modifier, + ), + charisma = StatUio( + label = R.string.character_sheet_stat_charisma, + value = sheet.charisma, + modifier = sheet.charisma.modifier, + ), + strengthSavingThrows = SavingsThrowsUio( + label = R.string.character_sheet_stat_strength, + multiplier = sheet.strengthSavingThrows, + modifier = sheet.strength.modifier + sheet.strengthSavingThrows * sheet.proficiency, + ), + dexteritySavingThrows = SavingsThrowsUio( + label = R.string.character_sheet_stat_dexterity, + multiplier = sheet.dexteritySavingThrows, + modifier = sheet.dexterity.modifier + sheet.dexteritySavingThrows * sheet.proficiency, + ), + constitutionSavingThrows = SavingsThrowsUio( + label = R.string.character_sheet_stat_constitution, + multiplier = sheet.constitutionSavingThrows, + modifier = sheet.constitution.modifier + sheet.constitutionSavingThrows * sheet.proficiency, + ), + intelligenceSavingThrows = SavingsThrowsUio( + label = R.string.character_sheet_stat_intelligence, + multiplier = sheet.intelligenceSavingThrows, + modifier = sheet.intelligence.modifier + sheet.intelligenceSavingThrows * sheet.proficiency, + ), + wisdomSavingThrows = SavingsThrowsUio( + label = R.string.character_sheet_stat_wisdom, + multiplier = sheet.wisdomSavingThrows, + modifier = sheet.wisdom.modifier + sheet.wisdomSavingThrows * sheet.proficiency, + ), + charismaSavingThrows = SavingsThrowsUio( + label = R.string.character_sheet_stat_charisma, + multiplier = sheet.charismaSavingThrows, + modifier = sheet.charisma.modifier + sheet.charismaSavingThrows * sheet.proficiency, + ), + acrobatics = ProficiencyUio( + label = R.string.character_sheet_proficiency_acrobatics, + related = R.string.character_sheet_stat_dexterity_short, + multiplier = sheet.acrobatics, + modifier = sheet.dexterity.modifier + sheet.acrobatics * sheet.proficiency, + ), + animalHandling = ProficiencyUio( + label = R.string.character_sheet_proficiency_animal_handling, + related = R.string.character_sheet_stat_wisdom_short, + multiplier = sheet.animalHandling, + modifier = sheet.wisdom.modifier + sheet.animalHandling * sheet.proficiency, + ), + arcana = ProficiencyUio( + label = R.string.character_sheet_proficiency_arcana, + related = R.string.character_sheet_stat_intelligence_short, + multiplier = sheet.arcana, + modifier = sheet.intelligence.modifier + sheet.arcana * sheet.proficiency, + ), + athletics = ProficiencyUio( + label = R.string.character_sheet_proficiency_athletics, + related = R.string.character_sheet_stat_strength_short, + multiplier = sheet.athletics, + modifier = sheet.strength.modifier + sheet.athletics * sheet.proficiency, + ), + deception = ProficiencyUio( + label = R.string.character_sheet_proficiency_deception, + related = R.string.character_sheet_stat_charisma_short, + multiplier = sheet.deception, + modifier = sheet.charisma.modifier + sheet.deception * sheet.proficiency, + ), + history = ProficiencyUio( + label = R.string.character_sheet_proficiency_history, + related = R.string.character_sheet_stat_intelligence_short, + multiplier = sheet.history, + modifier = sheet.intelligence.modifier + sheet.history * sheet.proficiency, + ), + insight = ProficiencyUio( + label = R.string.character_sheet_proficiency_insight, + related = R.string.character_sheet_stat_wisdom_short, + multiplier = sheet.insight, + modifier = sheet.wisdom.modifier + sheet.insight * sheet.proficiency, + ), + intimidation = ProficiencyUio( + label = R.string.character_sheet_proficiency_intimidation, + related = R.string.character_sheet_stat_charisma_short, + multiplier = sheet.intimidation, + modifier = sheet.charisma.modifier + sheet.intimidation * sheet.proficiency, + ), + investigation = ProficiencyUio( + label = R.string.character_sheet_proficiency_investigation, + related = R.string.character_sheet_stat_intelligence_short, + multiplier = sheet.investigation, + modifier = sheet.intelligence.modifier + sheet.investigation * sheet.proficiency, + ), + medicine = ProficiencyUio( + label = R.string.character_sheet_proficiency_medicine, + related = R.string.character_sheet_stat_wisdom_short, + multiplier = sheet.medicine, + modifier = sheet.wisdom.modifier + sheet.medicine * sheet.proficiency, + ), + nature = ProficiencyUio( + label = R.string.character_sheet_proficiency_nature, + related = R.string.character_sheet_stat_intelligence_short, + multiplier = sheet.nature, + modifier = sheet.intelligence.modifier + sheet.nature * sheet.proficiency, + ), + perception = ProficiencyUio( + label = R.string.character_sheet_proficiency_perception, + related = R.string.character_sheet_stat_wisdom_short, + multiplier = sheet.perception, + modifier = sheet.wisdom.modifier + sheet.perception * sheet.proficiency, + ), + performance = ProficiencyUio( + label = R.string.character_sheet_proficiency_performance, + related = R.string.character_sheet_stat_charisma_short, + multiplier = sheet.performance, + modifier = sheet.charisma.modifier + sheet.performance * sheet.proficiency, + ), + persuasion = ProficiencyUio( + label = R.string.character_sheet_proficiency_persuasion, + related = R.string.character_sheet_stat_charisma_short, + multiplier = sheet.persuasion, + modifier = sheet.charisma.modifier + sheet.persuasion * sheet.proficiency, + ), + religion = ProficiencyUio( + label = R.string.character_sheet_proficiency_religion, + related = R.string.character_sheet_stat_intelligence_short, + multiplier = sheet.religion, + modifier = sheet.intelligence.modifier + sheet.religion * sheet.proficiency, + ), + sleightOfHand = ProficiencyUio( + label = R.string.character_sheet_proficiency_sleight_of_hand, + related = R.string.character_sheet_stat_dexterity_short, + multiplier = sheet.sleightOfHand, + modifier = sheet.dexterity.modifier + sheet.sleightOfHand * sheet.proficiency, + ), + stealth = ProficiencyUio( + label = R.string.character_sheet_proficiency_stealth, + related = R.string.character_sheet_stat_dexterity_short, + multiplier = sheet.stealth, + modifier = sheet.dexterity.modifier + sheet.stealth * sheet.proficiency, + ), + survival = ProficiencyUio( + label = R.string.character_sheet_proficiency_survival, + related = R.string.character_sheet_stat_wisdom_short, + multiplier = sheet.survival, + modifier = sheet.wisdom.modifier + sheet.survival * sheet.proficiency, + ), + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/facotry/ConvertRollIntoDisplayableFactory.kt b/app/src/main/java/com/pixelized/rplexicon/facotry/ConvertRollIntoDisplayableFactory.kt new file mode 100644 index 0000000..4b76b1a --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/facotry/ConvertRollIntoDisplayableFactory.kt @@ -0,0 +1,125 @@ +package com.pixelized.rplexicon.facotry + +import com.pixelized.rplexicon.model.Roll +import com.pixelized.rplexicon.ui.screens.rolls.composable.RollDiceUio +import com.pixelized.rplexicon.ui.screens.rolls.composable.ThrowsCardUio +import com.pixelized.rplexicon.utilitary.extentions.icon +import com.pixelized.rplexicon.utilitary.extentions.toLabel +import javax.inject.Inject +import kotlin.math.max +import kotlin.math.min + +class ConvertRollIntoDisplayableFactory @Inject constructor() { + + fun roll(roll: Roll): Pair { + + val mainDices = roll.dices.firstOrNull() + val bonusDicesList = roll.dices.subList(fromIndex = 1, toIndex = roll.dices.size) + val allRolledValues = mutableListOf() + + val (rollDice, rollDetail) = mainDices?.let { dice -> + val (label, sum) = dice.roll() + allRolledValues.add(sum) + + val diceIcon = dice.toRollCardUio(result = sum) + val cardDetail = dice.toThrowsCardUio(roll = label, result = sum) + + diceIcon to listOf(cardDetail) + } ?: (null to emptyList()) + + val isCriticalSuccess = rollDice?.isCriticalSuccess ?: false + val isCriticalFailure = rollDice?.isCriticalFailure ?: false + + val diceBonus = bonusDicesList.map { dice -> + val (label, sum) = dice.roll() + allRolledValues.add(sum) + + dice.toThrowsCardUio( + roll = label, + result = sum, + ) + } + + val flatBonus = roll.bonus.map { bonus -> + allRolledValues.add(bonus.value) + + ThrowsCardUio.Detail( + title = bonus.label, + result = "${bonus.value}", + ) + } + + return rollDice to ThrowsCardUio( + title = roll.title, + highlight = roll.highlight, + dice = mainDices?.faces?.icon, + roll = allRolledValues.toLabel(), + result = when { + isCriticalSuccess -> (mainDices?.faces ?: 20).toString() + isCriticalFailure -> "1" + else -> "${allRolledValues.sum()}" + }, + isCriticalSuccess = isCriticalSuccess, + isCriticalFailure = isCriticalFailure, + details = rollDetail + diceBonus + flatBonus, + ) + } +} + +private fun Roll.Dice.toRollCardUio(result: Int) = RollDiceUio( + icon = faces.icon, + isCriticalSuccess = count == 1 && result == faces, + isCriticalFailure = count == 1 && result == 1, + result = "$result", +) + +private fun Roll.Dice.toThrowsCardUio(roll: String, result: Int) = ThrowsCardUio.Detail( + title = title, + throws = ThrowsCardUio.Throw( + dice = faces.icon, + advantage = advantage, + disadvantage = disadvantage, + roll = label, + result = roll, + ), + result = "$result", +) + +private data class RollResult( + val label: String, + val sum: Int, +) + +private fun Roll.Dice.roll(): RollResult { + return when { + advantage -> { + val roll = List(count) { + (Math.random() * faces + 1).toInt() to (Math.random() * faces + 1).toInt() + } + RollResult( + label = roll.joinToString(" + ") { "${it.first}~${it.second}" }, + sum = roll.sumOf { max(it.first, it.second) }, + ) + } + + disadvantage -> { + val roll = List(count) { + (Math.random() * faces + 1).toInt() to (Math.random() * faces + 1).toInt() + } + RollResult( + label = roll.joinToString(" + ") { "${it.first}~${it.second}" }, + sum = roll.sumOf { min(it.first, it.second) }, + ) + } + + else -> { + val roll = List(count) { + (Math.random() * faces + 1).toInt() + } + RollResult( + label = roll.toLabel(), + sum = roll.sum(), + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/facotry/RollParser.kt b/app/src/main/java/com/pixelized/rplexicon/facotry/RollParser.kt index 152049d..82b4b3d 100644 --- a/app/src/main/java/com/pixelized/rplexicon/facotry/RollParser.kt +++ b/app/src/main/java/com/pixelized/rplexicon/facotry/RollParser.kt @@ -2,6 +2,7 @@ package com.pixelized.rplexicon.facotry import com.pixelized.rplexicon.model.CharacterSheet import com.pixelized.rplexicon.model.Roll +import com.pixelized.rplexicon.utilitary.extentions.modifier import javax.inject.Inject class RollParser @Inject constructor() { @@ -17,14 +18,15 @@ class RollParser @Inject constructor() { val bonus = bonusRegex.findAll(item).map { bonus -> Roll.Bonus( label = bonus.value, - bonus = bonus.value.parseBonus(characterSheet = characterSheet), + value = bonus.value.parseBonus(characterSheet = characterSheet), ) } dices.toList() to bonus.toList() } ?: (null to null) return Roll( - label = label.toString(), + title = label.toString(), + highlight = null, dices = dices ?: emptyList(), bonus = bonus ?: emptyList(), ) @@ -34,14 +36,13 @@ class RollParser @Inject constructor() { characterSheet: CharacterSheet, ): Int = when (this?.lowercase()) { "bonus" -> characterSheet.proficiency - "force" -> characterSheet.strengthBonus + "force" -> characterSheet.strength.modifier else -> 0 } private fun MatchResult.parseDice(): Roll.Dice { val (count, faces) = destructured return Roll.Dice( - label = value, count = count.toIntOrNull() ?: 0, faces = faces.toIntOrNull() ?: 0, ) diff --git a/app/src/main/java/com/pixelized/rplexicon/model/CharacterSheet.kt b/app/src/main/java/com/pixelized/rplexicon/model/CharacterSheet.kt index 0ed7116..10842c2 100644 --- a/app/src/main/java/com/pixelized/rplexicon/model/CharacterSheet.kt +++ b/app/src/main/java/com/pixelized/rplexicon/model/CharacterSheet.kt @@ -1,8 +1,5 @@ package com.pixelized.rplexicon.model -import androidx.compose.runtime.Stable - -@Stable data class CharacterSheet( val hitPoint: Int, // Point de vie val armorClass: Int, // Classe d'armure @@ -37,35 +34,7 @@ data class CharacterSheet( val sleightOfHand: Int, // DEX, Représentation val stealth: Int, // DEX, Survie val survival: Int, // WIS, Tromperie -) { - val proficiencyBonus: Int = kotlin.math.floor(proficiency / 2 - 5f).toInt() - val strengthBonus: Int = kotlin.math.floor(strength / 2 - 5f).toInt() - val dexterityBonus: Int = kotlin.math.floor(dexterity / 2 - 5f).toInt() - val constitutionBonus: Int = kotlin.math.floor(constitution / 2 - 5f).toInt() - val intelligenceBonus: Int = kotlin.math.floor(intelligence / 2 - 5f).toInt() -} - -@Stable -data class Roll( - val label: String, - val dices: List, - val bonus: List, -) { - @Stable - data class Dice( - val label: String, - val count: Int, - val faces: Int, - ) - - @Stable - data class Bonus( - val label: String, - val bonus: Int, - ) -} - - +) diff --git a/app/src/main/java/com/pixelized/rplexicon/model/Roll.kt b/app/src/main/java/com/pixelized/rplexicon/model/Roll.kt new file mode 100644 index 0000000..2bccd99 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/model/Roll.kt @@ -0,0 +1,108 @@ +package com.pixelized.rplexicon.model + +import androidx.compose.runtime.Stable + +@Stable +data class Roll( + val title: String, + val highlight: String? = null, + val dices: List, + val bonus: List, +) { + @Stable + data class Dice( + val title: String? = null, + val advantage: Boolean = false, + val disadvantage: Boolean = false, + val count: Int, + val faces: Int, + ) { + val label: String = "${count}d${faces}" + + companion object { + fun d20( + title: String? = null, + advantage: Boolean = false, + disadvantage: Boolean = false, + amount: Int = 1, + ) = Dice( + title = title, + advantage = advantage, + disadvantage = disadvantage, + count = amount, + faces = 20, + ) + + fun d12( + title: String? = null, + advantage: Boolean = false, + disadvantage: Boolean = false, + amount: Int = 1, + ) = Dice( + title = title, + advantage = advantage, + disadvantage = disadvantage, + count = amount, + faces = 12, + ) + + fun d10( + title: String? = null, + advantage: Boolean = false, + disadvantage: Boolean = false, + amount: Int = 1, + ) = Dice( + title = title, + advantage = advantage, + disadvantage = disadvantage, + count = amount, + faces = 10, + ) + + fun d8( + title: String? = null, + advantage: Boolean = false, + disadvantage: Boolean = false, + amount: Int = 1, + ) = Dice( + title = title, + advantage = advantage, + disadvantage = disadvantage, + count = amount, + faces = 8, + ) + + fun d6( + title: String? = null, + advantage: Boolean = false, + disadvantage: Boolean = false, + amount: Int = 1, + ) = Dice( + title = title, + advantage = advantage, + disadvantage = disadvantage, + count = amount, + faces = 6, + ) + + fun d4( + title: String? = null, + advantage: Boolean = false, + disadvantage: Boolean = false, + amount: Int = 1, + ) = Dice( + title = title, + advantage = advantage, + disadvantage = disadvantage, + count = amount, + faces = 4, + ) + } + } + + @Stable + data class Bonus( + val label: String, + val value: Int, + ) +} diff --git a/app/src/main/java/com/pixelized/rplexicon/repository/CharacterSheetRepository.kt b/app/src/main/java/com/pixelized/rplexicon/repository/CharacterSheetRepository.kt index 6c7c48e..aa7ac07 100644 --- a/app/src/main/java/com/pixelized/rplexicon/repository/CharacterSheetRepository.kt +++ b/app/src/main/java/com/pixelized/rplexicon/repository/CharacterSheetRepository.kt @@ -1,12 +1,13 @@ package com.pixelized.rplexicon.repository -import android.util.Log import com.google.api.services.sheets.v4.model.ValueRange import com.pixelized.rplexicon.facotry.RollParser import com.pixelized.rplexicon.model.CharacterSheet import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure import com.pixelized.rplexicon.utilitary.exceptions.ServiceNotReady import com.pixelized.rplexicon.utilitary.extentions.sheet +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import javax.inject.Inject import javax.inject.Singleton @@ -15,6 +16,13 @@ class CharacterSheetRepository @Inject constructor( private val googleRepository: GoogleSheetServiceRepository, private val rollParser: RollParser, ) { + private val _data = MutableStateFlow>(emptyMap()) + val data: StateFlow> get() = _data + + fun find(name: String?): CharacterSheet? { + return name?.let { _data.value[name] } + } + @Throws(ServiceNotReady::class, IncompatibleSheetStructure::class, Exception::class) suspend fun fetchCharacterSheet() { googleRepository.fetch { sheet -> @@ -27,8 +35,8 @@ class CharacterSheetRepository @Inject constructor( @Throws(IncompatibleSheetStructure::class) private fun updateData(data: ValueRange?) { val sheet = data?.values?.sheet() - var id = 0 + var id = 0 val bru = sheet?.map { (it as? List<*>)?.get(1) } val characterSheet = CharacterSheet( @@ -41,12 +49,18 @@ class CharacterSheetRepository @Inject constructor( intelligence = (bru?.get(Sheet.INTELLIGENCE) as? String)?.toIntOrNull() ?: 0, wisdom = (bru?.get(Sheet.WISDOM) as? String)?.toIntOrNull() ?: 0, charisma = (bru?.get(Sheet.CHARISMA) as? String)?.toIntOrNull() ?: 0, - strengthSavingThrows = (bru?.get(Sheet.STRENGTH_SAVING_THROWS) as? String)?.toIntOrNull() ?: 0, - dexteritySavingThrows = (bru?.get(Sheet.DEXTERITY_SAVING_THROWS) as? String)?.toIntOrNull() ?: 0, - constitutionSavingThrows = (bru?.get(Sheet.CONSTITUTION_SAVING_THROWS) as? String)?.toIntOrNull() ?: 0, - intelligenceSavingThrows = (bru?.get(Sheet.INTELLIGENCE_SAVING_THROWS) as? String)?.toIntOrNull() ?: 0, - wisdomSavingThrows = (bru?.get(Sheet.WISDOM_SAVING_THROWS) as? String)?.toIntOrNull() ?: 0, - charismaSavingThrows = (bru?.get(Sheet.CHARISMA_SAVING_THROWS) as? String)?.toIntOrNull() ?: 0, + strengthSavingThrows = (bru?.get(Sheet.STRENGTH_SAVING_THROWS) as? String)?.toIntOrNull() + ?: 0, + dexteritySavingThrows = (bru?.get(Sheet.DEXTERITY_SAVING_THROWS) as? String)?.toIntOrNull() + ?: 0, + constitutionSavingThrows = (bru?.get(Sheet.CONSTITUTION_SAVING_THROWS) as? String)?.toIntOrNull() + ?: 0, + intelligenceSavingThrows = (bru?.get(Sheet.INTELLIGENCE_SAVING_THROWS) as? String)?.toIntOrNull() + ?: 0, + wisdomSavingThrows = (bru?.get(Sheet.WISDOM_SAVING_THROWS) as? String)?.toIntOrNull() + ?: 0, + charismaSavingThrows = (bru?.get(Sheet.CHARISMA_SAVING_THROWS) as? String)?.toIntOrNull() + ?: 0, acrobatics = (bru?.get(Sheet.ACROBATICS) as? String)?.toIntOrNull() ?: 0, animalHandling = (bru?.get(Sheet.ANIMAL_HANDLING) as? String)?.toIntOrNull() ?: 0, arcana = (bru?.get(Sheet.ARCANA) as? String)?.toIntOrNull() ?: 0, @@ -71,8 +85,9 @@ class CharacterSheetRepository @Inject constructor( rollParser.parseRoll(characterSheet = characterSheet, value = it?.toString()) } - Log.e(TAG, characterSheet.toString()) - Log.e(TAG, rolls.toString()) + _data.tryEmit( + hashMapOf("bru" to characterSheet) + ) } companion object { @@ -98,23 +113,23 @@ class CharacterSheetRepository @Inject constructor( const val INTELLIGENCE_SAVING_THROWS = 13 const val WISDOM_SAVING_THROWS = 14 const val CHARISMA_SAVING_THROWS = 15 - const val ACROBATICS = 16 - const val ANIMAL_HANDLING = 17 - const val ARCANA = 18 - const val ATHLETICS = 19 - const val DECEPTION = 20 - const val HISTORY = 21 - const val INSIGHT = 22 - const val INTIMIDATION = 23 - const val INVESTIGATION = 24 - const val MEDICINE = 25 - const val NATURE = 26 - const val PERCEPTION = 27 - const val PERFORMANCE = 28 - const val PERSUASION = 29 - const val RELIGION = 30 - const val SLEIGHT_OF_HAND = 31 - const val STEALTH = 32 - const val SURVIVAL = 33 + const val ACROBATICS = 16 // Acrobaties + const val ARCANA = 17 // Arcanes + const val ATHLETICS = 18 // Athlétisme + const val STEALTH = 19 // Discrétion + const val ANIMAL_HANDLING = 20 // Dressage + const val SLEIGHT_OF_HAND = 21 // Escamotage + const val HISTORY = 22 // Histoire + const val INTIMIDATION = 23 // Intimidation + const val INSIGHT = 24 // Intuition + const val INVESTIGATION = 25 // Investigation + const val MEDICINE = 26 // Médecine + const val NATURE = 27 // Nature + const val PERCEPTION = 28 // Perception + const val PERSUASION = 29 // Persuasion + const val RELIGION = 30 // Religion + const val PERFORMANCE = 31 // Représentation + const val SURVIVAL = 32 // Survie + const val DECEPTION = 33 // Tromperie } } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/composable/BlurredOverlayHost.kt b/app/src/main/java/com/pixelized/rplexicon/ui/composable/BlurredOverlayHost.kt new file mode 100644 index 0000000..739d642 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/composable/BlurredOverlayHost.kt @@ -0,0 +1,97 @@ +package com.pixelized.rplexicon.ui.composable + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.blur +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.dp +import com.pixelized.rplexicon.utilitary.extentions.clickableInterceptor +import com.pixelized.rplexicon.utilitary.extentions.lexicon + +@Stable +interface BlurredOverlayHostState { + val isOverlayVisible: Boolean + fun showOverlay() + fun hideOverlay() +} + +@Stable +private class BlurredOverlayHostStateImpl( + rollOverlayVisibilityState: MutableState, +) : BlurredOverlayHostState { + override var isOverlayVisible by rollOverlayVisibilityState + + override fun showOverlay() { + isOverlayVisible = true + } + + override fun hideOverlay() { + isOverlayVisible = false + } +} + +@Composable +@Stable +fun rememberBlurredOverlayHostState(): BlurredOverlayHostState { + val rollOverlayVisibilityState = rememberSaveable { mutableStateOf(false) } + return remember { + BlurredOverlayHostStateImpl( + rollOverlayVisibilityState = rollOverlayVisibilityState + ) + } +} + +@Composable +fun BlurredOverlayHost( + rollOverlayState: BlurredOverlayHostState = rememberBlurredOverlayHostState(), + overlay: @Composable () -> Unit, + content: @Composable () -> Unit, +) { + val density = LocalDensity.current + + Surface { + val blurs = animateDpAsState( + targetValue = if (rollOverlayState.isOverlayVisible) 4.dp else 0.dp, + label = "RollOverlayHostBlurAnimation", + ) + Box( + modifier = Modifier.blur(radius = blurs.value), + content = { content() }, + ) + AnimatedVisibility( + visible = rollOverlayState.isOverlayVisible, + enter = fadeIn() + slideInVertically { with(density) { 64.dp.roundToPx() } }, + exit = fadeOut() + slideOutVertically { with(density) { 64.dp.roundToPx() } }, + content = { + Box( + modifier = Modifier + .clickableInterceptor() + .background(brush = MaterialTheme.lexicon.colorScheme.rollOverlayBrush) + .fillMaxSize() + .systemBarsPadding(), + ) { + overlay() + } + }, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/navigation/HomeNavHost.kt b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/HomeNavHost.kt index 39e381a..d584933 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/navigation/HomeNavHost.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/HomeNavHost.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.lazy.LazyListState import androidx.compose.material3.BottomAppBar import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Scaffold @@ -27,6 +28,7 @@ import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.rememberNavController +import com.pixelized.rplexicon.LocalRollOverlay import com.pixelized.rplexicon.LocalSnack import com.pixelized.rplexicon.R import com.pixelized.rplexicon.ui.navigation.pages.LEXICON_LIST_ROUTE @@ -38,6 +40,7 @@ import com.pixelized.rplexicon.ui.navigation.pages.composableQuests import com.pixelized.rplexicon.ui.navigation.pages.navigateToLexicon import com.pixelized.rplexicon.ui.navigation.pages.navigateToLocation import com.pixelized.rplexicon.ui.navigation.pages.navigateToQuestList +import com.pixelized.rplexicon.ui.navigation.screens.navigateToCharacterSheet val LocalPageNavHost = staticCompositionLocalOf { error("LocalScreenNavHost not ready") @@ -62,6 +65,15 @@ fun HomeNavHost( title = { Text(text = stringResource(id = R.string.app_name)) }, + actions = { + val screen = LocalScreenNavHost.current + IconButton(onClick = { screen.navigateToCharacterSheet() }) { + Icon( + painter = painterResource(id = R.drawable.ic_d20_24), + contentDescription = null + ) + } + } ) }, snackbarHost = { diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/navigation/ScreenNavHost.kt b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/ScreenNavHost.kt index 322f196..0fa4d2d 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/navigation/ScreenNavHost.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/ScreenNavHost.kt @@ -11,6 +11,7 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.rememberNavController import com.pixelized.rplexicon.ui.navigation.screens.AUTHENTICATION_ROUTE import com.pixelized.rplexicon.ui.navigation.screens.composableAuthentication +import com.pixelized.rplexicon.ui.navigation.screens.composableCharacterSheet import com.pixelized.rplexicon.ui.navigation.screens.composableHome import com.pixelized.rplexicon.ui.navigation.screens.composableLexiconDetail import com.pixelized.rplexicon.ui.navigation.screens.composableLexiconSearch @@ -47,6 +48,7 @@ fun ScreenNavHost( composableLexiconSearch() composableQuestDetail() composableLocationDetail() + composableCharacterSheet() } } } diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/navigation/screens/ComposableCharacterSheet.kt b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/screens/ComposableCharacterSheet.kt new file mode 100644 index 0000000..5104bae --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/screens/ComposableCharacterSheet.kt @@ -0,0 +1,27 @@ +package com.pixelized.rplexicon.ui.navigation.screens + +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavHostController +import androidx.navigation.NavOptionsBuilder +import com.pixelized.rplexicon.ui.navigation.NavigationAnimation +import com.pixelized.rplexicon.ui.navigation.animatedComposable +import com.pixelized.rplexicon.ui.screens.character.CharacterSheetScreen + +private const val ROUTE = "characterSheet" +const val CHARACTER_SHEET_ROUTE = ROUTE + +fun NavGraphBuilder.composableCharacterSheet() { + animatedComposable( + route = CHARACTER_SHEET_ROUTE, + animation = NavigationAnimation.Push, + ) { + CharacterSheetScreen() + } +} + +fun NavHostController.navigateToCharacterSheet( + option: NavOptionsBuilder.() -> Unit = {}, +) { + val route = ROUTE + navigate(route = route, builder = option) +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/CharacterSheetScreen.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/CharacterSheetScreen.kt new file mode 100644 index 0000000..4f255c7 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/CharacterSheetScreen.kt @@ -0,0 +1,706 @@ +package com.pixelized.rplexicon.ui.screens.character + +import android.content.res.Configuration +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.pixelized.rplexicon.LocalRollOverlay +import com.pixelized.rplexicon.NO_WINDOW_INSETS +import com.pixelized.rplexicon.R +import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost +import com.pixelized.rplexicon.ui.screens.character.composable.Proficiency +import com.pixelized.rplexicon.ui.screens.character.composable.ProficiencyUio +import com.pixelized.rplexicon.ui.screens.character.composable.SavingsThrows +import com.pixelized.rplexicon.ui.screens.character.composable.SavingsThrowsUio +import com.pixelized.rplexicon.ui.screens.character.composable.Stat +import com.pixelized.rplexicon.ui.screens.character.composable.StatUio +import com.pixelized.rplexicon.ui.theme.LexiconTheme + +@Stable +data class CharacterSheetUio( + val strength: StatUio, + val dexterity: StatUio, + val constitution: StatUio, + val intelligence: StatUio, + val wisdom: StatUio, + val charisma: StatUio, + val strengthSavingThrows: SavingsThrowsUio, + val dexteritySavingThrows: SavingsThrowsUio, + val constitutionSavingThrows: SavingsThrowsUio, + val intelligenceSavingThrows: SavingsThrowsUio, + val wisdomSavingThrows: SavingsThrowsUio, + val charismaSavingThrows: SavingsThrowsUio, + val acrobatics: ProficiencyUio, + val animalHandling: ProficiencyUio, + val arcana: ProficiencyUio, + val athletics: ProficiencyUio, + val deception: ProficiencyUio, + val history: ProficiencyUio, + val insight: ProficiencyUio, + val intimidation: ProficiencyUio, + val investigation: ProficiencyUio, + val medicine: ProficiencyUio, + val nature: ProficiencyUio, + val perception: ProficiencyUio, + val performance: ProficiencyUio, + val persuasion: ProficiencyUio, + val religion: ProficiencyUio, + val sleightOfHand: ProficiencyUio, + val stealth: ProficiencyUio, + val survival: ProficiencyUio, +) + +@Composable +fun CharacterSheetScreen( + viewModel: CharacterSheetViewModel = hiltViewModel(), +) { + val screen = LocalScreenNavHost.current + val overlay = LocalRollOverlay.current + + Surface( + modifier = Modifier.fillMaxSize(), + ) { + viewModel.sheet.value?.let { + CharacterSheetContent( + modifier = Modifier + .fillMaxSize() + .systemBarsPadding(), + state = rememberScrollState(), + sheet = it, + onBack = { + screen.popBackStack() + }, + onStrength = { + val roll = viewModel.strengthRoll() + overlay.prepareRoll(roll = roll) + overlay.showOverlay() + }, + onDexterity = { + val roll = viewModel.dexterityRoll() + overlay.prepareRoll(roll = roll) + overlay.showOverlay() + }, + onConstitution = { + val roll = viewModel.constitutionRoll() + overlay.prepareRoll(roll = roll) + overlay.showOverlay() + }, + onIntelligence = { + val roll = viewModel.intelligenceRoll() + overlay.prepareRoll(roll = roll) + overlay.showOverlay() + }, + onWisdom = { + val roll = viewModel.wisdomRoll() + overlay.prepareRoll(roll = roll) + overlay.showOverlay() + }, + onCharisma = { + val roll = viewModel.charismaRoll() + overlay.prepareRoll(roll = roll) + overlay.showOverlay() + }, + onStrengthSavingThrows = { + val roll = viewModel.strengthSavingThrowsRoll() + overlay.prepareRoll(roll = roll) + overlay.showOverlay() + }, + onDexteritySavingThrows = { + val roll = viewModel.dexteritySavingThrowsRoll() + overlay.prepareRoll(roll = roll) + overlay.showOverlay() + }, + onConstitutionSavingThrows = { + val roll = viewModel.constitutionSavingThrowsRoll() + overlay.prepareRoll(roll = roll) + overlay.showOverlay() + }, + onIntelligenceSavingThrows = { + val roll = viewModel.intelligenceSavingThrowsRoll() + overlay.prepareRoll(roll = roll) + overlay.showOverlay() + }, + onWisdomSavingThrows = { + val roll = viewModel.wisdomSavingThrowsRoll() + overlay.prepareRoll(roll = roll) + overlay.showOverlay() + }, + onCharismaSavingThrows = { + val roll = viewModel.charismaSavingThrowsRoll() + overlay.prepareRoll(roll = roll) + overlay.showOverlay() + }, + onAcrobatics = { + val roll = viewModel.acrobaticsRoll() + overlay.prepareRoll(roll = roll) + overlay.showOverlay() + }, + onAnimalHandling = { + val roll = viewModel.animalHandlingRoll() + overlay.prepareRoll(roll = roll) + overlay.showOverlay() + }, + onArcana = { + val roll = viewModel.arcanaRoll() + overlay.prepareRoll(roll = roll) + overlay.showOverlay() + }, + onAthletics = { + val roll = viewModel.athleticsRoll() + overlay.prepareRoll(roll = roll) + overlay.showOverlay() + }, + onDeception = { + val roll = viewModel.deceptionRoll() + overlay.prepareRoll(roll = roll) + overlay.showOverlay() + }, + onHistory = { + val roll = viewModel.historyRoll() + overlay.prepareRoll(roll = roll) + overlay.showOverlay() + }, + onInsight = { + val roll = viewModel.insightRoll() + overlay.prepareRoll(roll = roll) + overlay.showOverlay() + }, + onIntimidation = { + val roll = viewModel.intimidationRoll() + overlay.prepareRoll(roll = roll) + overlay.showOverlay() + }, + onInvestigation = { + val roll = viewModel.investigationRoll() + overlay.prepareRoll(roll = roll) + overlay.showOverlay() + }, + onMedicine = { + val roll = viewModel.medicineRoll() + overlay.prepareRoll(roll = roll) + overlay.showOverlay() + }, + onNature = { + val roll = viewModel.natureRoll() + overlay.prepareRoll(roll = roll) + overlay.showOverlay() + }, + onPerception = { + val roll = viewModel.perceptionRoll() + overlay.prepareRoll(roll = roll) + overlay.showOverlay() + }, + onPerformance = { + val roll = viewModel.performanceRoll() + overlay.prepareRoll(roll = roll) + overlay.showOverlay() + }, + onPersuasion = { + val roll = viewModel.persuasionRoll() + overlay.prepareRoll(roll = roll) + overlay.showOverlay() + }, + onReligion = { + val roll = viewModel.religionRoll() + overlay.prepareRoll(roll = roll) + overlay.showOverlay() + }, + onSleightOfHand = { + val roll = viewModel.sleightOfHandRoll() + overlay.prepareRoll(roll = roll) + overlay.showOverlay() + }, + onStealth = { + val roll = viewModel.stealthRoll() + overlay.prepareRoll(roll = roll) + overlay.showOverlay() + }, + onSurvival = { + val roll = viewModel.survivalRoll() + overlay.prepareRoll(roll = roll) + overlay.showOverlay() + }, + ) + } + + BackHandler(enabled = overlay.isOverlayVisible) { + overlay.hideOverlay() + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun CharacterSheetContent( + modifier: Modifier = Modifier, + state: ScrollState, + sheet: CharacterSheetUio, + onBack: () -> Unit, + onStrength: () -> Unit, + onDexterity: () -> Unit, + onConstitution: () -> Unit, + onIntelligence: () -> Unit, + onWisdom: () -> Unit, + onCharisma: () -> Unit, + onStrengthSavingThrows: () -> Unit, + onDexteritySavingThrows: () -> Unit, + onConstitutionSavingThrows: () -> Unit, + onIntelligenceSavingThrows: () -> Unit, + onWisdomSavingThrows: () -> Unit, + onCharismaSavingThrows: () -> Unit, + onAcrobatics: () -> Unit, + onAnimalHandling: () -> Unit, + onArcana: () -> Unit, + onAthletics: () -> Unit, + onDeception: () -> Unit, + onHistory: () -> Unit, + onInsight: () -> Unit, + onIntimidation: () -> Unit, + onInvestigation: () -> Unit, + onMedicine: () -> Unit, + onNature: () -> Unit, + onPerception: () -> Unit, + onPerformance: () -> Unit, + onPersuasion: () -> Unit, + onReligion: () -> Unit, + onSleightOfHand: () -> Unit, + onStealth: () -> Unit, + onSurvival: () -> Unit, +) { + Scaffold( + modifier = modifier, + containerColor = Color.Transparent, + contentWindowInsets = NO_WINDOW_INSETS, + topBar = { + TopAppBar( + navigationIcon = { + IconButton(onClick = onBack) { + Icon( + painter = painterResource(id = R.drawable.ic_arrow_back_ios_new_24), + contentDescription = null + ) + } + }, + title = { + Text( + text = stringResource(id = R.string.character_sheet_title), + ) + }, + ) + }, + ) { paddingValues -> + Surface( + modifier = Modifier + .verticalScroll(state = state) + .padding(paddingValues = paddingValues), + ) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Row( + modifier = Modifier.padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + Stat( + modifier = Modifier.weight(1f), + stat = sheet.strength, + onClick = onStrength, + ) + Stat( + modifier = Modifier.weight(1f), + stat = sheet.dexterity, + onClick = onDexterity, + ) + Stat( + modifier = Modifier.weight(1f), + stat = sheet.constitution, + onClick = onConstitution, + ) + } + Row( + modifier = Modifier.padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + Stat( + modifier = Modifier.weight(1f), + stat = sheet.intelligence, + onClick = onIntelligence, + ) + Stat( + modifier = Modifier.weight(1f), + stat = sheet.wisdom, + onClick = onWisdom, + ) + Stat( + modifier = Modifier.weight(1f), + stat = sheet.charisma, + onClick = onCharisma, + ) + } + } + + Row( + modifier = Modifier.padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + SavingsThrows( + savingsThrows = sheet.strengthSavingThrows, + onClick = onStrengthSavingThrows, + ) + SavingsThrows( + savingsThrows = sheet.dexteritySavingThrows, + onClick = onDexteritySavingThrows, + ) + SavingsThrows( + savingsThrows = sheet.constitutionSavingThrows, + onClick = onConstitutionSavingThrows, + ) + } + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + SavingsThrows( + savingsThrows = sheet.intelligenceSavingThrows, + onClick = onIntelligenceSavingThrows, + ) + SavingsThrows( + savingsThrows = sheet.wisdomSavingThrows, + onClick = onWisdomSavingThrows, + ) + SavingsThrows( + savingsThrows = sheet.charismaSavingThrows, + onClick = onCharismaSavingThrows, + ) + } + } + + Column { + Proficiency( + modifier = Modifier.clickable(onClick = onAcrobatics), + proficiency = sheet.acrobatics, + ) + Proficiency( + modifier = Modifier.clickable(onClick = onAnimalHandling), + proficiency = sheet.animalHandling, + ) + Proficiency( + modifier = Modifier.clickable(onClick = onArcana), + proficiency = sheet.arcana, + ) + Proficiency( + modifier = Modifier.clickable(onClick = onAthletics), + proficiency = sheet.athletics, + ) + Proficiency( + modifier = Modifier.clickable(onClick = onDeception), + proficiency = sheet.deception, + ) + Proficiency( + modifier = Modifier.clickable(onClick = onHistory), + proficiency = sheet.history, + ) + Proficiency( + modifier = Modifier.clickable(onClick = onInsight), + proficiency = sheet.insight, + ) + Proficiency( + modifier = Modifier.clickable(onClick = onIntimidation), + proficiency = sheet.intimidation, + ) + Proficiency( + modifier = Modifier.clickable(onClick = onInvestigation), + proficiency = sheet.investigation, + ) + Proficiency( + modifier = Modifier.clickable(onClick = onMedicine), + proficiency = sheet.medicine, + ) + Proficiency( + modifier = Modifier.clickable(onClick = onNature), + proficiency = sheet.nature, + ) + Proficiency( + modifier = Modifier.clickable(onClick = onPerception), + proficiency = sheet.perception, + ) + Proficiency( + modifier = Modifier.clickable(onClick = onPerformance), + proficiency = sheet.performance, + ) + Proficiency( + modifier = Modifier.clickable(onClick = onPersuasion), + proficiency = sheet.persuasion, + ) + Proficiency( + modifier = Modifier.clickable(onClick = onReligion), + proficiency = sheet.religion, + ) + Proficiency( + modifier = Modifier.clickable(onClick = onSleightOfHand), + proficiency = sheet.sleightOfHand, + ) + Proficiency( + modifier = Modifier.clickable(onClick = onStealth), + proficiency = sheet.stealth, + ) + Proficiency( + modifier = Modifier.clickable(onClick = onSurvival), + proficiency = sheet.survival, + ) + } + } + } + } +} + +@Composable +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun CharacterScreenPreview() { + LexiconTheme { + val bru = remember { + CharacterSheetUio( + strength = StatUio( + label = R.string.character_sheet_stat_strength, + value = 16, + modifier = 3, + ), + dexterity = StatUio( + label = R.string.character_sheet_stat_dexterity, + value = 10, + modifier = 0, + ), + constitution = StatUio( + label = R.string.character_sheet_stat_constitution, + value = 16, + modifier = 3, + ), + intelligence = StatUio( + label = R.string.character_sheet_stat_intelligence, + value = 8, + modifier = 1, + ), + wisdom = StatUio( + label = R.string.character_sheet_stat_wisdom, + value = 14, + modifier = 2, + ), + charisma = StatUio( + label = R.string.character_sheet_stat_charisma, + value = 10, + modifier = 0 + ), + strengthSavingThrows = SavingsThrowsUio( + label = R.string.character_sheet_stat_strength, + multiplier = 1, + modifier = 5, + ), + dexteritySavingThrows = SavingsThrowsUio( + label = R.string.character_sheet_stat_dexterity, + multiplier = 0, + modifier = 0, + ), + constitutionSavingThrows = SavingsThrowsUio( + label = R.string.character_sheet_stat_constitution, + multiplier = 1, + modifier = +5, + ), + intelligenceSavingThrows = SavingsThrowsUio( + label = R.string.character_sheet_stat_intelligence, + multiplier = 0, + modifier = -1, + ), + wisdomSavingThrows = SavingsThrowsUio( + label = R.string.character_sheet_stat_wisdom, + multiplier = 0, + modifier = +2, + ), + charismaSavingThrows = SavingsThrowsUio( + label = R.string.character_sheet_stat_charisma, + multiplier = 0, + modifier = +0, + ), + acrobatics = ProficiencyUio( + label = R.string.character_sheet_proficiency_acrobatics, + related = R.string.character_sheet_stat_dexterity_short, + multiplier = 0, + modifier = +0, + ), + animalHandling = ProficiencyUio( + label = R.string.character_sheet_proficiency_animal_handling, + related = R.string.character_sheet_stat_wisdom_short, + multiplier = 0, + modifier = 2, + ), + arcana = ProficiencyUio( + label = R.string.character_sheet_proficiency_arcana, + related = R.string.character_sheet_stat_intelligence_short, + multiplier = 0, + modifier = -1, + ), + athletics = ProficiencyUio( + label = R.string.character_sheet_proficiency_athletics, + related = R.string.character_sheet_stat_strength_short, + multiplier = 1, + modifier = 5, + ), + deception = ProficiencyUio( + label = R.string.character_sheet_proficiency_deception, + related = R.string.character_sheet_stat_charisma_short, + multiplier = 0, + modifier = 0, + ), + history = ProficiencyUio( + label = R.string.character_sheet_proficiency_history, + related = R.string.character_sheet_stat_intelligence_short, + multiplier = 0, + modifier = -1, + ), + insight = ProficiencyUio( + label = R.string.character_sheet_proficiency_insight, + related = R.string.character_sheet_stat_wisdom_short, + multiplier = 1, + modifier = 4, + ), + intimidation = ProficiencyUio( + label = R.string.character_sheet_proficiency_intimidation, + related = R.string.character_sheet_stat_charisma_short, + multiplier = 1, + modifier = 2, + ), + investigation = ProficiencyUio( + label = R.string.character_sheet_proficiency_investigation, + related = R.string.character_sheet_stat_intelligence_short, + multiplier = 0, + modifier = -1, + ), + medicine = ProficiencyUio( + label = R.string.character_sheet_proficiency_medicine, + related = R.string.character_sheet_stat_wisdom_short, + multiplier = 0, + modifier = 2, + ), + nature = ProficiencyUio( + label = R.string.character_sheet_proficiency_nature, + related = R.string.character_sheet_stat_intelligence_short, + multiplier = 0, + modifier = -1, + ), + perception = ProficiencyUio( + label = R.string.character_sheet_proficiency_perception, + related = R.string.character_sheet_stat_wisdom_short, + multiplier = 1, + modifier = 4, + ), + performance = ProficiencyUio( + label = R.string.character_sheet_proficiency_performance, + related = R.string.character_sheet_stat_charisma_short, + multiplier = 0, + modifier = 0, + ), + persuasion = ProficiencyUio( + label = R.string.character_sheet_proficiency_persuasion, + related = R.string.character_sheet_stat_charisma_short, + multiplier = 0, + modifier = 0, + ), + religion = ProficiencyUio( + label = R.string.character_sheet_proficiency_religion, + related = R.string.character_sheet_stat_intelligence_short, + multiplier = 0, + modifier = -1, + ), + sleightOfHand = ProficiencyUio( + label = R.string.character_sheet_proficiency_sleight_of_hand, + related = R.string.character_sheet_stat_dexterity_short, + multiplier = 0, + modifier = 0, + ), + stealth = ProficiencyUio( + label = R.string.character_sheet_proficiency_stealth, + related = R.string.character_sheet_stat_dexterity_short, + multiplier = 0, + modifier = 0, + ), + survival = ProficiencyUio( + label = R.string.character_sheet_proficiency_survival, + related = R.string.character_sheet_stat_wisdom_short, + multiplier = 1, + modifier = 4, + ), + ) + } + + Surface { + CharacterSheetContent( + modifier = Modifier.fillMaxSize(), + state = rememberScrollState(), + sheet = bru, + onBack = { }, + onStrength = { }, + onDexterity = { }, + onConstitution = { }, + onIntelligence = { }, + onWisdom = { }, + onCharisma = { }, + onStrengthSavingThrows = { }, + onDexteritySavingThrows = { }, + onConstitutionSavingThrows = { }, + onIntelligenceSavingThrows = { }, + onWisdomSavingThrows = { }, + onCharismaSavingThrows = { }, + onAcrobatics = { }, + onAnimalHandling = { }, + onArcana = { }, + onAthletics = { }, + onDeception = { }, + onHistory = { }, + onInsight = { }, + onIntimidation = { }, + onInvestigation = { }, + onMedicine = { }, + onNature = { }, + onPerception = { }, + onPerformance = { }, + onPersuasion = { }, + onReligion = { }, + onSleightOfHand = { }, + onStealth = { }, + onSurvival = { }, + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/CharacterSheetViewModel.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/CharacterSheetViewModel.kt new file mode 100644 index 0000000..d84f47b --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/CharacterSheetViewModel.kt @@ -0,0 +1,321 @@ +package com.pixelized.rplexicon.ui.screens.character + +import android.app.Application +import androidx.annotation.StringRes +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import com.pixelized.rplexicon.R +import com.pixelized.rplexicon.facotry.ConvertCharacterSheetIntoDisplayableFactory +import com.pixelized.rplexicon.model.CharacterSheet +import com.pixelized.rplexicon.model.Roll +import com.pixelized.rplexicon.repository.CharacterSheetRepository +import com.pixelized.rplexicon.utilitary.extentions.context +import com.pixelized.rplexicon.utilitary.extentions.modifier +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class CharacterSheetViewModel @Inject constructor( + application: Application, + repository: CharacterSheetRepository, + factory: ConvertCharacterSheetIntoDisplayableFactory, +) : AndroidViewModel(application = application) { + + private lateinit var model: CharacterSheet + private val _sheet = mutableStateOf(null) + val sheet: State get() = _sheet + + init { + viewModelScope.launch { + repository.fetchCharacterSheet() + _sheet.value = repository.find("bru")?.let { + model = it + factory.toUio(sheet = it) + } + } + } + + fun strengthRoll(): Roll = statRoll( + abilityRes = R.string.character_sheet_stat_strength, + abilityValue = model.strength, + ) + + fun dexterityRoll(): Roll = statRoll( + abilityRes = R.string.character_sheet_stat_dexterity, + abilityValue = model.dexterity, + ) + + fun constitutionRoll(): Roll = statRoll( + abilityRes = R.string.character_sheet_stat_constitution, + abilityValue = model.constitution, + ) + + fun intelligenceRoll(): Roll = statRoll( + abilityRes = R.string.character_sheet_stat_intelligence, + abilityValue = model.intelligence, + ) + + fun wisdomRoll(): Roll = statRoll( + abilityRes = R.string.character_sheet_stat_wisdom, + abilityValue = model.wisdom, + ) + + fun charismaRoll(): Roll = statRoll( + abilityRes = R.string.character_sheet_stat_charisma, + abilityValue = model.charisma, + ) + + fun strengthSavingThrowsRoll(): Roll = savingThrowRoll( + abilityRes = R.string.character_sheet_stat_strength, + abilityValue = model.strength, + masteryLevel = model.strengthSavingThrows, + ) + + fun dexteritySavingThrowsRoll(): Roll = savingThrowRoll( + abilityRes = R.string.character_sheet_stat_dexterity, + abilityValue = model.dexterity, + masteryLevel = model.dexteritySavingThrows, + ) + + fun constitutionSavingThrowsRoll(): Roll = savingThrowRoll( + abilityRes = R.string.character_sheet_stat_constitution, + abilityValue = model.constitution, + masteryLevel = model.constitutionSavingThrows, + ) + + fun intelligenceSavingThrowsRoll(): Roll = savingThrowRoll( + abilityRes = R.string.character_sheet_stat_intelligence, + abilityValue = model.intelligence, + masteryLevel = model.intelligenceSavingThrows, + ) + + fun wisdomSavingThrowsRoll(): Roll = savingThrowRoll( + abilityRes = R.string.character_sheet_stat_wisdom, + abilityValue = model.wisdom, + masteryLevel = model.wisdomSavingThrows, + ) + + fun charismaSavingThrowsRoll(): Roll = savingThrowRoll( + abilityRes = R.string.character_sheet_stat_charisma, + abilityValue = model.charisma, + masteryLevel = model.charismaSavingThrows, + ) + + fun acrobaticsRoll(): Roll = abilityRoll( + abilityRes = R.string.character_sheet_proficiency_acrobatics, + relatedRes = R.string.character_sheet_stat_dexterity, + abilityValue = model.dexterity, + masteryLevel = model.acrobatics, + ) + + fun animalHandlingRoll(): Roll = abilityRoll( + abilityRes = R.string.character_sheet_proficiency_animal_handling, + relatedRes = R.string.character_sheet_stat_wisdom, + abilityValue = model.wisdom, + masteryLevel = model.animalHandling, + ) + + fun arcanaRoll(): Roll = abilityRoll( + abilityRes = R.string.character_sheet_proficiency_arcana, + relatedRes = R.string.character_sheet_stat_intelligence, + abilityValue = model.intelligence, + masteryLevel = model.arcana, + ) + + fun athleticsRoll(): Roll = abilityRoll( + abilityRes = R.string.character_sheet_proficiency_athletics, + relatedRes = R.string.character_sheet_stat_strength, + abilityValue = model.strength, + masteryLevel = model.athletics, + ) + + fun deceptionRoll(): Roll = abilityRoll( + abilityRes = R.string.character_sheet_proficiency_deception, + relatedRes = R.string.character_sheet_stat_charisma, + abilityValue = model.charisma, + masteryLevel = model.deception, + ) + + fun historyRoll(): Roll = abilityRoll( + abilityRes = R.string.character_sheet_proficiency_history, + relatedRes = R.string.character_sheet_stat_intelligence, + abilityValue = model.intelligence, + masteryLevel = model.history, + ) + + fun insightRoll(): Roll = abilityRoll( + abilityRes = R.string.character_sheet_proficiency_insight, + relatedRes = R.string.character_sheet_stat_wisdom, + abilityValue = model.wisdom, + masteryLevel = model.insight, + ) + + fun intimidationRoll(): Roll = abilityRoll( + abilityRes = R.string.character_sheet_proficiency_intimidation, + relatedRes = R.string.character_sheet_stat_charisma, + abilityValue = model.charisma, + masteryLevel = model.intimidation, + ) + + fun investigationRoll(): Roll = abilityRoll( + abilityRes = R.string.character_sheet_proficiency_investigation, + relatedRes = R.string.character_sheet_stat_intelligence, + abilityValue = model.intelligence, + masteryLevel = model.investigation, + ) + + fun medicineRoll(): Roll = abilityRoll( + abilityRes = R.string.character_sheet_proficiency_medicine, + relatedRes = R.string.character_sheet_stat_wisdom, + abilityValue = model.wisdom, + masteryLevel = model.medicine, + ) + + fun natureRoll(): Roll = abilityRoll( + abilityRes = R.string.character_sheet_proficiency_nature, + relatedRes = R.string.character_sheet_stat_intelligence, + abilityValue = model.intelligence, + masteryLevel = model.nature, + ) + + fun perceptionRoll(): Roll = abilityRoll( + abilityRes = R.string.character_sheet_proficiency_perception, + relatedRes = R.string.character_sheet_stat_wisdom, + abilityValue = model.wisdom, + masteryLevel = model.perception, + ) + + fun performanceRoll(): Roll = abilityRoll( + abilityRes = R.string.character_sheet_proficiency_performance, + relatedRes = R.string.character_sheet_stat_charisma, + abilityValue = model.charisma, + masteryLevel = model.performance, + ) + + fun persuasionRoll(): Roll = abilityRoll( + abilityRes = R.string.character_sheet_proficiency_persuasion, + relatedRes = R.string.character_sheet_stat_charisma, + abilityValue = model.charisma, + masteryLevel = model.persuasion, + ) + + fun religionRoll(): Roll = abilityRoll( + abilityRes = R.string.character_sheet_proficiency_religion, + relatedRes = R.string.character_sheet_stat_intelligence, + abilityValue = model.intelligence, + masteryLevel = model.religion, + ) + + fun sleightOfHandRoll(): Roll = abilityRoll( + abilityRes = R.string.character_sheet_proficiency_sleight_of_hand, + relatedRes = R.string.character_sheet_stat_dexterity, + abilityValue = model.dexterity, + masteryLevel = model.sleightOfHand, + ) + + fun stealthRoll(): Roll = abilityRoll( + abilityRes = R.string.character_sheet_proficiency_stealth, + relatedRes = R.string.character_sheet_stat_dexterity, + abilityValue = model.dexterity, + masteryLevel = model.stealth, + ) + + fun survivalRoll(): Roll = abilityRoll( + abilityRes = R.string.character_sheet_proficiency_survival, + relatedRes = R.string.character_sheet_stat_wisdom, + abilityValue = model.wisdom, + masteryLevel = model.survival, + ) + +// ////////////////////////////////////// +// region: Helpers + + private fun statRoll( + abilityRes: Int, + abilityValue: Int, + ): Roll { + val ability = context.getString(abilityRes) + return Roll( + title = context.getString(R.string.dice_roll_check_title, ability.uppercase()), + highlight = ability, + dices = listOf( + Roll.Dice.d20( + title = context.getString(R.string.dice_roll_check_detail, ability) + ), + ), + bonus = listOf( + Roll.Bonus( + label = context.getString(R.string.dice_roll_bonus_detail, ability), + value = abilityValue.modifier + ) + ), + ) + } + + private fun savingThrowRoll( + @StringRes abilityRes: Int, + abilityValue: Int, + masteryLevel: Int, + masteryValue: Int = model.proficiency, + ): Roll { + val ability = context.getString(abilityRes) + return Roll( + title = context.getString(R.string.dice_roll_saving_throw_title, ability.uppercase()), + highlight = ability, + dices = listOf( + Roll.Dice.d20( + title = context.getString(R.string.dice_roll_saving_throw_detail, ability) + ), + ), + bonus = listOf( + Roll.Bonus( + label = context.getString( + if (masteryLevel == 2) R.string.dice_roll_mastery_expertise else R.string.dice_roll_mastery_proficiency, + context.getString(R.string.dice_roll_mastery_saving_throw) + ), + value = masteryValue * masteryLevel, + ), + Roll.Bonus( + label = context.getString(R.string.dice_roll_bonus_detail, ability), + value = abilityValue.modifier + ) + ), + ) + } + + private fun abilityRoll( + @StringRes abilityRes: Int, + @StringRes relatedRes: Int, + abilityValue: Int, + masteryLevel: Int, + masteryValue: Int = model.proficiency, + ): Roll { + val ability = context.getString(abilityRes) + val related = context.getString(relatedRes) + return Roll( + title = context.getString(R.string.dice_roll_check_title, ability.uppercase()), + highlight = ability, + dices = listOf( + Roll.Dice.d20( + title = context.getString(R.string.dice_roll_check_detail, ability) + ), + ), + bonus = listOf( + Roll.Bonus( + label = context.getString( + if (masteryLevel == 2) R.string.dice_roll_mastery_expertise else R.string.dice_roll_mastery_proficiency, + ability + ), + value = masteryValue * masteryLevel, + ), + Roll.Bonus( + label = context.getString(R.string.dice_roll_bonus_detail, related), + value = abilityValue.modifier + ) + ), + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/MasteryCircle.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/MasteryCircle.kt new file mode 100644 index 0000000..7cee3ae --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/MasteryCircle.kt @@ -0,0 +1,50 @@ +package com.pixelized.rplexicon.ui.screens.character.composable + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +@Composable +fun MasteryCircle( + modifier: Modifier = Modifier, + multiplier: Int, + size: Dp = 12.dp, +) { + when (multiplier) { + 0 -> Box( + modifier = modifier + .size(size = size) + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.onSurface, + shape = CircleShape + ) + ) + + 1 -> Box( + modifier = modifier + .size(size = size) + .background(color = MaterialTheme.colorScheme.onSurface, shape = CircleShape) + ) + + else -> Box( + modifier = modifier + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.onSurface, + shape = CircleShape + ) + .padding(2.dp) + ) { + MasteryCircle(multiplier = multiplier - 1) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/Proficiency.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/Proficiency.kt new file mode 100644 index 0000000..27b495e --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/Proficiency.kt @@ -0,0 +1,120 @@ +package com.pixelized.rplexicon.ui.screens.character.composable + +import android.content.res.Configuration +import androidx.annotation.StringRes +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.pixelized.rplexicon.R +import com.pixelized.rplexicon.ui.theme.LexiconTheme +import com.pixelized.rplexicon.utilitary.extentions.toLabel + +@Stable +data class ProficiencyUio( + @StringRes val label: Int, // the proficiency label + @StringRes val related: Int, // the related stat to the proficiency + val multiplier: Int, // the multiplier (use for the circle) 0 -> None 1 -> Mastery 2 -> Expertise + val modifier: Int, // the proficiency value. (stats + mastery) +) + +@Composable +fun Proficiency( + modifier: Modifier = Modifier, + padding: PaddingValues = PaddingValues(start = 16.dp, end = 27.dp), + proficiency: ProficiencyUio, +) { + Box( + modifier = modifier.heightIn(48.dp), + contentAlignment = Alignment.CenterStart, + ) { + Row( + modifier = Modifier.padding(paddingValues = padding), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + Box( + modifier = Modifier.size(size = 32.dp), + contentAlignment = Alignment.Center, + ) { + MasteryCircle( + multiplier = proficiency.multiplier, + ) + } + + Row( + modifier = Modifier.weight(1f), + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + modifier = Modifier.alignByBaseline(), + style = MaterialTheme.typography.bodyMedium, + text = stringResource(id = proficiency.label), + ) + Text( + modifier = Modifier.alignByBaseline(), + style = MaterialTheme.typography.labelSmall, + text = "(${stringResource(proficiency.related)})", + ) + } + + Text( + style = MaterialTheme.typography.titleMedium, + text = proficiency.modifier.toLabel(), + ) + } + } +} + +@Composable +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun ProficiencyPreview( + @PreviewParameter(ProficiencyPreviewProvider::class) proficiency: ProficiencyUio, +) { + LexiconTheme { + Surface { + Proficiency( + proficiency = proficiency, + ) + } + } +} + +private class ProficiencyPreviewProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf( + ProficiencyUio( + label = R.string.character_sheet_proficiency_acrobatics, + related = R.string.character_sheet_stat_dexterity_short, + multiplier = 0, + modifier = -1 + ), + ProficiencyUio( + label = R.string.character_sheet_proficiency_athletics, + related = R.string.character_sheet_stat_strength_short, + multiplier = 1, + modifier = 5, + ), + ProficiencyUio( + label = R.string.character_sheet_proficiency_sleight_of_hand, + related = R.string.character_sheet_stat_dexterity_short, + multiplier = 2, + modifier = 8, + ), + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/SavingThrows.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/SavingThrows.kt new file mode 100644 index 0000000..2722abb --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/SavingThrows.kt @@ -0,0 +1,116 @@ +package com.pixelized.rplexicon.ui.screens.character.composable + +import android.content.res.Configuration +import androidx.annotation.StringRes +import androidx.compose.foundation.clickable +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.shape.CutCornerShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.pixelized.rplexicon.R +import com.pixelized.rplexicon.ui.theme.LexiconTheme +import com.pixelized.rplexicon.utilitary.extentions.ddBorder +import com.pixelized.rplexicon.utilitary.extentions.toLabel + +@Stable +data class SavingsThrowsUio( + @StringRes val label: Int, // the proficiency label + val multiplier: Int, // the multiplier (use for the circle) 0 -> None 1 -> Mastery 2 -> Expertise + val modifier: Int, // the proficiency value. (stats + mastery) +) + +@Composable +fun SavingsThrows( + modifier: Modifier = Modifier, + padding: PaddingValues = PaddingValues(start = 8.dp, top = 8.dp, bottom = 8.dp, end = 8.dp), + savingsThrows: SavingsThrowsUio, + onClick: () -> Unit, +) { + Row( + modifier = modifier + .ddBorder( + horizontalSpacing = 3.dp, + outline = remember { CutCornerShape(size = 24.dp) }, + inner = remember { RoundedCornerShape(size = 24.dp) }, + ) + .clickable(onClick = onClick) + .padding(paddingValues = padding), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + MasteryCircle( + multiplier = savingsThrows.multiplier, + ) + + Text( + modifier = Modifier.weight(weight = 1f, fill = true), + style = MaterialTheme.typography.bodyMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + text = stringResource(id = savingsThrows.label), + ) + + Text( + style = MaterialTheme.typography.titleMedium, + text = savingsThrows.modifier.toLabel(), + ) + } +} + +@Composable +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +fun SavingsThrowsPreview( + @PreviewParameter(SavingsThrowsPreviewProvider::class) preview: SavingsThrowsUio, +) { + LexiconTheme { + Surface { + SavingsThrows( + modifier = Modifier.padding(all = 8.dp), + savingsThrows = preview, + onClick = { }, + ) + } + } +} + +private class SavingsThrowsPreviewProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf( + SavingsThrowsUio( + label = R.string.character_sheet_stat_strength, + multiplier = 0, + modifier = 0, + ), + SavingsThrowsUio( + label = R.string.character_sheet_stat_intelligence, + multiplier = 1, + modifier = 5, + ), + SavingsThrowsUio( + label = R.string.character_sheet_stat_dexterity, + multiplier = 2, + modifier = 8, + ), + SavingsThrowsUio( + label = R.string.character_sheet_stat_charisma, + multiplier = 0, + modifier = -1, + ), + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/Stat.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/Stat.kt new file mode 100644 index 0000000..d8ed374 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/Stat.kt @@ -0,0 +1,89 @@ +package com.pixelized.rplexicon.ui.screens.character.composable + +import android.content.res.Configuration.UI_MODE_NIGHT_NO +import android.content.res.Configuration.UI_MODE_NIGHT_YES +import androidx.annotation.StringRes +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CutCornerShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Divider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.pixelized.rplexicon.R +import com.pixelized.rplexicon.ui.theme.LexiconTheme +import com.pixelized.rplexicon.utilitary.extentions.ddBorder +import com.pixelized.rplexicon.utilitary.extentions.toLabel + +@Stable +data class StatUio( + @StringRes val label: Int, + val value: Int, + val modifier: Int, +) + +@Composable +fun Stat( + modifier: Modifier = Modifier, + stat: StatUio, + onClick: () -> Unit +) { + Column( + modifier = modifier + .ddBorder( + inner = remember { RoundedCornerShape(size = 8.dp) }, + outline = remember { CutCornerShape(size = 16.dp) }, + ) + .clickable(onClick = onClick) + .padding(all = 8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + style = MaterialTheme.typography.labelSmall, + text = stringResource(id = stat.label), + ) + Text( + style = MaterialTheme.typography.displayMedium, + text = stat.modifier.toLabel(), + ) + Divider( + modifier = Modifier.width(width = 32.dp), + ) + Text( + style = MaterialTheme.typography.labelMedium, + text = "${stat.value}", + ) + } +} + +@Composable +@Preview(uiMode = UI_MODE_NIGHT_NO) +@Preview(uiMode = UI_MODE_NIGHT_YES) +private fun StatPreview() { + LexiconTheme { + Surface { + Stat( + modifier = Modifier.padding(all = 8.dp), + stat = StatUio( + label = R.string.character_sheet_stat_constitution, + value = 16, + modifier = 3, + ), + onClick = { }, + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/RollOverlay.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/RollOverlay.kt new file mode 100644 index 0000000..0bf4933 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/RollOverlay.kt @@ -0,0 +1,233 @@ +package com.pixelized.rplexicon.ui.screens.rolls + +import android.content.res.Configuration.UI_MODE_NIGHT_NO +import android.content.res.Configuration.UI_MODE_NIGHT_YES +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.SizeTransform +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Close +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.Stable +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.pixelized.rplexicon.LocalRollOverlay +import com.pixelized.rplexicon.R +import com.pixelized.rplexicon.model.Roll +import com.pixelized.rplexicon.ui.composable.BlurredOverlayHostState +import com.pixelized.rplexicon.ui.screens.rolls.composable.RollDice +import com.pixelized.rplexicon.ui.screens.rolls.composable.RollDiceUio +import com.pixelized.rplexicon.ui.screens.rolls.composable.ThrowsCard +import com.pixelized.rplexicon.ui.screens.rolls.composable.ThrowsCardUio +import com.pixelized.rplexicon.ui.theme.LexiconTheme + +@Composable +fun RollOverlay( + viewModel: RollOverlayViewModel = hiltViewModel(), +) { + val overlay = LocalRollOverlay.current + + RollOverlayContent( + modifier = Modifier.fillMaxSize(), + dice = viewModel.dice, + card = viewModel.card, + showDetail = viewModel.showDetail, + onDice = { + viewModel.roll() + }, + onCard = { + viewModel.toggleDetail() + }, + onClose = { + overlay.hideOverlay() + } + ) +} + +@Composable +private fun RollOverlayContent( + modifier: Modifier = Modifier, + dice: State, + card: State, + showDetail: State, + onDice: () -> Unit, + onCard: () -> Unit, + onClose: () -> Unit, +) { + val density = LocalDensity.current + + Box( + modifier = modifier, + ) { + RollDice( + modifier = Modifier + .align(alignment = Alignment.Center) + .padding(horizontal = 16.dp), + dice = dice, + onDice = onDice, + ) + + AnimatedContent( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter) + .padding(all = 16.dp), + targetState = card.value, + transitionSpec = { + val enter = fadeIn() + slideInVertically { with(density) { 64.dp.roundToPx() } } + val exit = fadeOut() + slideOutVertically { with(density) { -64.dp.roundToPx() } } + val transform = SizeTransform(clip = false) + enter togetherWith exit using transform + }, + label = "RollOverlayDisplay", + ) { + when (it) { + null -> Box( + modifier = Modifier.fillMaxWidth(), + ) + + else -> ThrowsCard( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onCard), + throws = it, + showDetail = showDetail, + ) + } + } + + IconButton( + modifier = Modifier + .align(alignment = Alignment.TopEnd) + .padding( + top = 6.dp, + end = 4.dp + ), + onClick = onClose + ) { + Icon( + imageVector = Icons.Outlined.Close, + contentDescription = null, + ) + } + } +} + +@Composable +@Preview(uiMode = UI_MODE_NIGHT_NO) +@Preview(uiMode = UI_MODE_NIGHT_YES) +private fun RollOverlayPreview() { + LexiconTheme { + Surface { + RollOverlayContent( + modifier = Modifier.fillMaxSize(), + dice = remember { + mutableStateOf( + RollDiceUio( + icon = R.drawable.ic_d20_24, + isCriticalSuccess = true, + result = "20", + ) + ) + }, + card = remember { + mutableStateOf( + ThrowsCardUio( + title = "PERCEPTION CHECK", + highlight = "CHECK", + dice = R.drawable.ic_d20_24, + roll = "$20 + 2 + 2", + result = "20", + isCriticalSuccess = true, + details = listOf( + ThrowsCardUio.Detail( + throws = ThrowsCardUio.Throw( + dice = R.drawable.ic_d20_24, + roll = "1d20", + result = "20" + ), + result = "20" + ), + ThrowsCardUio.Detail( + title = "Wisdom bonus", + result = "2", + ), + ThrowsCardUio.Detail( + title = "Proficiency bonus", + result = "2", + ), + ), + ) + ) + }, + showDetail = remember { + mutableStateOf(true) + }, + onDice = { }, + onCard = { }, + onClose = { }, + ) + } + } +} + +interface BlurredRollOverlayHostState : BlurredOverlayHostState { + fun prepareRoll(roll: Roll) +} + +@Stable +private class BlurredRollOverlayHostStateImpl( + private val viewModel: RollOverlayViewModel, + rollOverlayVisibilityState: MutableState, +) : BlurredRollOverlayHostState { + override var isOverlayVisible by rollOverlayVisibilityState + + override fun prepareRoll(roll: Roll) { + viewModel.prepareRoll(roll) + } + + override fun showOverlay() { + isOverlayVisible = true + } + + override fun hideOverlay() { + isOverlayVisible = false + } +} + +@Composable +@Stable +fun rememberBlurredRollOverlayHostState( + viewModel: RollOverlayViewModel, +): BlurredRollOverlayHostState { + val rollOverlayVisibilityState = rememberSaveable { mutableStateOf(false) } + return remember { + BlurredRollOverlayHostStateImpl( + viewModel = viewModel, + rollOverlayVisibilityState = rollOverlayVisibilityState + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/RollOverlayViewModel.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/RollOverlayViewModel.kt new file mode 100644 index 0000000..8710a68 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/RollOverlayViewModel.kt @@ -0,0 +1,63 @@ +package com.pixelized.rplexicon.ui.screens.rolls + +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.pixelized.rplexicon.facotry.ConvertRollIntoDisplayableFactory +import com.pixelized.rplexicon.model.Roll +import com.pixelized.rplexicon.ui.screens.rolls.composable.RollDiceUio +import com.pixelized.rplexicon.ui.screens.rolls.composable.ThrowsCardUio +import com.pixelized.rplexicon.utilitary.extentions.icon +import com.pixelized.rplexicon.utilitary.extentions.switch +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class RollOverlayViewModel @Inject constructor( + private val factory: ConvertRollIntoDisplayableFactory, +) : ViewModel() { + + private var rollJob: Job? = null + private lateinit var roll: Roll + + private val _dice = mutableStateOf(RollDiceUio()) + val dice: State get() = _dice + + private val _card = mutableStateOf(null) + val card: State get() = _card + + private val _showDetail = mutableStateOf(false) + val showDetail: State get() = _showDetail + + fun prepareRoll(roll: Roll) { + this.roll = roll + _dice.value = RollDiceUio(icon = roll.dices.maxOf { it.faces }.icon) + _card.value = null + } + + fun roll() { + rollJob?.cancel() + rollJob = viewModelScope.launch { + val (dice, card) = factory.roll(roll = roll) + // Start the roll animation. + _dice.value = _dice.value.copy( + animationSpec = RollDiceUio.TWEEN, + rotation = _dice.value.rotation + 5f, + ) + delay(RollDiceUio.ROLL_DURATION) + // Roll the d20 value. + dice?.let { _dice.value = it } + delay(250) + // Display the roll card. + _card.value = card + } + } + + fun toggleDetail() { + _showDetail.switch() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/composable/RollDice.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/composable/RollDice.kt new file mode 100644 index 0000000..c6f15de --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/composable/RollDice.kt @@ -0,0 +1,220 @@ +package com.pixelized.rplexicon.ui.screens.rolls.composable + +import androidx.annotation.DrawableRes +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.LinearOutSlowInEasing +import androidx.compose.animation.core.SnapSpec +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.animation.expandIn +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkOut +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.State +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import com.pixelized.rplexicon.R +import com.pixelized.rplexicon.ui.theme.LexiconTheme +import com.pixelized.rplexicon.utilitary.extentions.lexicon + +@Stable +data class RollDiceUio( + @DrawableRes val icon: Int = R.drawable.ic_d20_24, + val rotation: Float = 0f, + val isCriticalSuccess: Boolean = false, + val isCriticalFailure: Boolean = false, + val result: String? = null, + val animationSpec: AnimationSpec = SNAP, +) { + companion object { + const val ROLL_DURATION = 750L + + val SNAP = SnapSpec() + + val TWEEN = tween( + durationMillis = ROLL_DURATION.toInt(), + easing = LinearOutSlowInEasing, + ) + } +} + +@Composable +fun RollDice( + modifier: Modifier = Modifier, + diceSize: Dp = 64.dp * 2.5f, + dice: State, + onDice: () -> Unit, +) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(space = 16.dp) + ) { + Box( + contentAlignment = Alignment.Center, + ) { + Dice( + modifier = Modifier + .size(size = diceSize) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = onDice, + ), + dice = dice, + ) + Result( + modifier = Modifier + .fillMaxWidth() + .height(height = diceSize), + dice = dice, + ) + } + Critical( + modifier = Modifier.fillMaxWidth(), + dice = dice, + ) + } +} + +@Composable +private fun Dice( + modifier: Modifier = Modifier, + dice: State, +) { + val animatedRotation = animateFloatAsState( + targetValue = dice.value.rotation * 360, + animationSpec = dice.value.animationSpec, + label = "AnimatedRotation" + ) + Icon( + modifier = modifier.rotate(degrees = animatedRotation.value % 360), + tint = MaterialTheme.colorScheme.primary, + painter = painterResource(id = dice.value.icon), + contentDescription = null, + ) +} + +@Composable +private fun Result( + modifier: Modifier = Modifier, + dice: State, +) { + val colorScheme = MaterialTheme.colorScheme + + AnimatedContent( + modifier = modifier, + targetState = dice.value.result, + transitionSpec = { + val enter = fadeIn() + expandIn(expandFrom = Alignment.Center) { it } + val exit = fadeOut() + shrinkOut(shrinkTowards = Alignment.Center) { IntSize.Zero } + enter togetherWith exit + }, + label = "" + ) { + when (it) { + null -> Box(modifier = Modifier) + else -> Box( + modifier = Modifier.background(brush = MaterialTheme.lexicon.colorScheme.dice.valueBackground), + contentAlignment = Alignment.Center, + ) { + Text( + style = MaterialTheme.typography.displayMedium, + text = it, + ) + } + } + } +} + +@Composable +private fun Critical( + modifier: Modifier = Modifier, + dice: State, +) { + val state = remember { + derivedStateOf { + when { + dice.value.isCriticalSuccess -> 1 + dice.value.isCriticalFailure -> 2 + else -> 0 + } + } + } + AnimatedContent( + modifier = modifier, + targetState = state.value, + transitionSpec = { + val enter = fadeIn() + expandIn(expandFrom = Alignment.Center) { it } + val exit = fadeOut() + shrinkOut(shrinkTowards = Alignment.Center) { IntSize.Zero } + enter togetherWith exit + }, + label = "", + ) { + Text( + style = MaterialTheme.typography.headlineLarge, + textAlign = TextAlign.Center, + color = when (it) { + 1 -> MaterialTheme.colorScheme.primary + 2 -> Color.Red + else -> MaterialTheme.colorScheme.onSurface + }, + text = when (it) { + 1 -> "SUCCÈS CRITIQUE" + 2 -> "ÉCHEC CRITIQUE" + else -> "" + }, + ) + } +} + +@Composable +@Preview +private fun SkillRollPreview() { + LexiconTheme { + Surface { + RollDice( + dice = remember { + mutableStateOf( + RollDiceUio( + icon = R.drawable.ic_d20_24, + rotation = 0f, + isCriticalSuccess = true, + result = "20", + ) + ) + }, + onDice = { }, + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/composable/ThrowsCard.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/composable/ThrowsCard.kt new file mode 100644 index 0000000..084f420 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/composable/ThrowsCard.kt @@ -0,0 +1,346 @@ +package com.pixelized.rplexicon.ui.screens.rolls.composable + +import android.content.res.Configuration +import androidx.annotation.DrawableRes +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +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.graphics.Color +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.rememberTextMeasurer +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.pixelized.rplexicon.R +import com.pixelized.rplexicon.ui.theme.LexiconTheme +import com.pixelized.rplexicon.utilitary.extentions.annotatedSpan +import com.pixelized.rplexicon.utilitary.extentions.highlightRegex +import java.util.UUID + +@Stable +data class ThrowsCardUio( + val id: String = UUID.randomUUID().toString(), + val title: String?, // the roll title + val highlight: String?, // the highlighted part of the title + @DrawableRes val dice: Int?, // the dice icon + val roll: String, // the roll + bonus string. + val result: String, // the final result + val isCriticalSuccess: Boolean = false, // highlight result + val isCriticalFailure: Boolean = false, // highlight result + val details: List, // the roll detail. +) { + @Stable + data class Detail( + val title: String? = null, // the detail title + val throws: Throw? = null, // the detail of the roll if any + val result: String, // the result + ) + + @Stable + data class Throw( + @DrawableRes val dice: Int, // the throw dice icon + val advantage: Boolean = false, // highlight if advantage + val disadvantage: Boolean = false, // highlight if disadvantage + val roll: String, // the roll (1d20, 2d20) + val result: String, // the result + ) +} + +@Composable +fun ThrowsCard( + modifier: Modifier = Modifier, + throws: ThrowsCardUio, + showDetail: State, +) { + val density = LocalDensity.current + val isDarkMode = isSystemInDarkTheme() + val colorScheme = MaterialTheme.colorScheme + val typography = MaterialTheme.typography + val highlight = remember { SpanStyle(color = colorScheme.primary) } + + val textMeasurer = rememberTextMeasurer() + val resultWidth = remember { + val measurer = textMeasurer.measure(text = "00", style = typography.displayMedium) + with(density) { measurer.size.width.toDp() } + } + + Surface( + modifier = modifier, + shape = RoundedCornerShape(size = 16.dp), + border = BorderStroke( + width = 1.dp, + color = MaterialTheme.colorScheme.onSurface.copy(0.25f) + ), + tonalElevation = if (isDarkMode) 4.dp else 0.dp, + ) { + Column( + modifier = Modifier + .padding(all = 16.dp) + .animateContentSize(), + ) { + throws.title?.let { + Text( + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.65f), + style = MaterialTheme.typography.bodyLarge, + text = AnnotatedString( + text = it, + spanStyles = throws.highlight?.highlightRegex?.annotatedSpan( + input = it, + spanStyle = highlight, + ) ?: emptyList() + ), + ) + } + + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + throws.dice?.let { + Icon( + modifier = Modifier.size(size = 32.dp), + tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.65f), + painter = painterResource(id = it), + contentDescription = null + ) + } + Text( + modifier = Modifier + .weight(1f) + .padding(start = 8.dp), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.titleLarge, + text = throws.roll, + ) + Text( + modifier = Modifier.padding(horizontal = 8.dp), + style = MaterialTheme.typography.headlineLarge, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.65f), + text = "=", + ) + Text( + modifier = Modifier.widthIn(min = resultWidth), + style = MaterialTheme.typography.displayMedium, + textAlign = TextAlign.Center, + color = when { + throws.isCriticalSuccess -> MaterialTheme.colorScheme.primary + throws.isCriticalFailure -> Color.Red + else -> MaterialTheme.colorScheme.onSurface + }, + text = throws.result, + ) + } + if (throws.details.isNotEmpty()) { + AnimatedVisibility( + visible = showDetail.value, + enter = fadeIn() + expandVertically(expandFrom = Alignment.Top) { it }, + exit = fadeOut() + shrinkVertically(shrinkTowards = Alignment.Top) { 0 }, + ) { + Column( + modifier = Modifier + .padding(top = 4.dp) + .fillMaxWidth(), + horizontalAlignment = Alignment.End, + ) { + throws.details.forEach { + ThrowsRollDetail( + detail = it, + resultWidth = resultWidth, + ) + } + } + } + } + } + } +} + +@Composable +private fun ThrowsRollDetail( + modifier: Modifier = Modifier, + detail: ThrowsCardUio.Detail, + resultWidth: Dp, +) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(space = 4.dp), + ) { + detail.title?.let { + Text( + modifier = Modifier.alignByBaseline(), + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.bodySmall, + text = it, + ) + } + detail.throws?.let { + Text( + modifier = Modifier.alignByBaseline(), + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.bodySmall, + textAlign = TextAlign.Center, + text = "(${it.roll})", + ) + Icon( + modifier = Modifier + .size(16.dp) + .align(alignment = Alignment.Bottom) + .padding(bottom = 2.dp), + tint = when { + it.advantage -> MaterialTheme.colorScheme.primary + it.disadvantage -> Color.Red + else -> MaterialTheme.colorScheme.onSurface + }, + painter = painterResource(id = it.dice), + contentDescription = null, + ) + Text( + modifier = Modifier + .alignByBaseline() + .weight(1f, false), + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.bodySmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + text = it.result, + ) + } + Text( + modifier = Modifier.alignByBaseline(), + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.bodySmall, + text = ":", + ) + Text( + modifier = Modifier + .alignByBaseline() + .width(width = resultWidth), + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.bodyLarge, + text = detail.result, + ) + } +} + +@Composable +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +fun RollToastPreview( + @PreviewParameter(RollToastPreviewProvider::class) preview: ThrowsCardUio, +) { + LexiconTheme { + val showDetail = remember { + mutableStateOf(true) + } + ThrowsCard( + modifier = Modifier.fillMaxWidth(), + throws = preview, + showDetail = showDetail, + ) + } +} + +private class RollToastPreviewProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf( + ThrowsCardUio( + title = "ATHLETICS CHECK", + highlight = "CHECK", + dice = R.drawable.ic_d20_24, + roll = "13 + 3 + 2", + result = "18", + details = emptyList(), + ), + ThrowsCardUio( + title = "INVESTIGATION CHECK", + highlight = "CHECK", + dice = R.drawable.ic_d20_24, + roll = "1 - 1", + result = "1", + isCriticalFailure = true, + details = listOf( + ThrowsCardUio.Detail( + throws = ThrowsCardUio.Throw( + dice = R.drawable.ic_d20_24, + roll = "1d20", + result = "1", + ), + result = "1", + ), + ThrowsCardUio.Detail( + title = "Intelligence bonus", + result = "-1", + ), + ), + ), + ThrowsCardUio( + title = "ATTACK HIT", + highlight = "HIT", + dice = R.drawable.ic_d20_24, + roll = "20 + 2 + 3 + 2", + result = "20", + isCriticalSuccess = true, + details = listOf( + ThrowsCardUio.Detail( + title = "Hit Check \"Advantage\"", + throws = ThrowsCardUio.Throw( + dice = R.drawable.ic_d20_24, + roll = "1d20", + advantage = true, + result = "20 ~ 5", + ), + result = "20", + ), + ThrowsCardUio.Detail( + title = "Benediction", + throws = ThrowsCardUio.Throw( + dice = R.drawable.ic_d4_24, + roll = "1d4", + result = "2", + ), + result = "2", + ), + ThrowsCardUio.Detail( + title = "Strength bonus", + result = "3", + ), + ThrowsCardUio.Detail( + title = "Mastery bonus", + result = "2", + ), + ), + ), + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/theme/colors/LexiconColors.kt b/app/src/main/java/com/pixelized/rplexicon/ui/theme/colors/LexiconColors.kt index a71b3d4..8caa618 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/theme/colors/LexiconColors.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/theme/colors/LexiconColors.kt @@ -5,18 +5,35 @@ import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color @Stable @Immutable class LexiconColors( val base: ColorScheme, + val placeholder: Color, val shadow: Color, val status: Color, val navigation: Color, - val placeholder: Color, val handle: Color, -) + val rollOverlayBrush: Brush, + val dice: Dice, + val characterSheet: CharacterSheet, +) { + @Stable + data class Dice( + val tint: Color, + val value: Color, + val valueBackground: Brush, + ) + + @Stable + data class CharacterSheet( + val innerBorder: Color, + val outlineBorder: Color, + ) +} @Stable fun darkColorScheme( @@ -27,17 +44,15 @@ fun darkColorScheme( onPrimary = Color.White, surfaceTint = Base.Purple80, ), - shadow: Color = ShadowPalette.shadow, - status: Color = Color.Transparent, - navigation: Color = Color.Transparent, placeholder: Color = Color(red = 49, green = 48, blue = 51), -) = LexiconColors( + sheet: LexiconColors.CharacterSheet = LexiconColors.CharacterSheet( + innerBorder = base.onSurface.copy(alpha = 0.35f), + outlineBorder = base.onSurface.copy(alpha = 0.6f), + ), +) = colorScheme( base = base, - shadow = shadow, - status = status, - navigation = navigation, placeholder = placeholder, - handle = base.onSurface.copy(alpha = 0.5f), + sheet = sheet ) @Stable @@ -49,10 +64,41 @@ fun lightColorScheme( onPrimary = Color.White, surfaceTint = Base.Purple40, ), + placeholder: Color = Color(red = 230, green = 225, blue = 229), + sheet: LexiconColors.CharacterSheet = LexiconColors.CharacterSheet( + innerBorder = base.onSurface.copy(alpha = 0.35f), + outlineBorder = base.onSurface.copy(alpha = 0.6f), + ), +) = colorScheme( + base = base, + placeholder = placeholder, + sheet = sheet, +) + +@Stable +fun colorScheme( + base: ColorScheme, + placeholder: Color, shadow: Color = ShadowPalette.shadow, status: Color = Color.Transparent, navigation: Color = Color.Transparent, - placeholder: Color = Color(red = 230, green = 225, blue = 229), + rollOverlayBrush: Brush = Brush.radialGradient( + colors = listOf( + base.surface.copy(alpha = 0.75f), + base.surface.copy(alpha = 0.25f), + ), + ), + dice: LexiconColors.Dice = LexiconColors.Dice( + tint = base.primary, + value = base.onSurface, + valueBackground = Brush.radialGradient( + colors = listOf( + base.surface.copy(alpha = 0.5f), + Color.Transparent, + ), + ) + ), + sheet: LexiconColors.CharacterSheet, ) = LexiconColors( base = base, shadow = shadow, @@ -60,4 +106,7 @@ fun lightColorScheme( navigation = navigation, placeholder = placeholder, handle = base.onSurface.copy(alpha = 0.5f), + rollOverlayBrush = rollOverlayBrush, + dice = dice, + characterSheet = sheet, ) diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/theme/typography/LexiconTypography.kt b/app/src/main/java/com/pixelized/rplexicon/ui/theme/typography/LexiconTypography.kt index c960b16..ca6f5ca 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/theme/typography/LexiconTypography.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/theme/typography/LexiconTypography.kt @@ -16,6 +16,7 @@ val regalFontFamily = FontFamily( Font(resId = R.font.regal, weight = FontWeight.Normal), ) +@Stable val stampFontFamily = FontFamily( Font(resId = R.font.rubber_stamp, weight = FontWeight.Normal), ) diff --git a/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/AndroidViewModelEx.kt b/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/AndroidViewModelEx.kt new file mode 100644 index 0000000..81c0719 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/AndroidViewModelEx.kt @@ -0,0 +1,6 @@ +package com.pixelized.rplexicon.utilitary.extentions + +import android.content.Context +import androidx.lifecycle.AndroidViewModel + +val AndroidViewModel.context: Context get() = getApplication() \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/IntEx.kt b/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/IntEx.kt new file mode 100644 index 0000000..56e8525 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/IntEx.kt @@ -0,0 +1,30 @@ +package com.pixelized.rplexicon.utilitary.extentions + +import com.pixelized.rplexicon.R +import kotlin.math.abs +import kotlin.math.floor + +val Int.icon + get() = when (this) { + 4 -> R.drawable.ic_d4_24 + 6 -> R.drawable.ic_d6_24 + 8 -> R.drawable.ic_d8_24 + 10 -> R.drawable.ic_d10_24 + 12 -> R.drawable.ic_d12_24 + else -> R.drawable.ic_d20_24 + } + +val Int.modifier: Int + get() = floor(this / 2 - 5f).toInt() + +fun Int.toLabel(): String = + "${this.signLabel}${abs(this)}" + +val Int.signLabel: String + get() = if (this < 0) "-" else "+" + +fun List.toLabel(): String = when (size) { + 0 -> "" + 1 -> "${abs(this[0])}" + else -> joinToString(" ") { "${it.signLabel} ${abs(it)}" }.substring(2) +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/ModifierEx.kt b/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/ModifierEx.kt index 6a06090..75dbadf 100644 --- a/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/ModifierEx.kt +++ b/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/ModifierEx.kt @@ -4,16 +4,24 @@ import androidx.compose.animation.core.FiniteAnimationSpec import androidx.compose.animation.core.Transition import androidx.compose.animation.core.spring import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.composed +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.platform.LocalDensity @@ -55,4 +63,59 @@ fun Modifier.scrollOffset( ): Modifier = composed { val density = LocalDensity.current this.offset(y = with(density) { block(scrollState.value.toDp()) }) +} + +fun Modifier.clickableInterceptor(): Modifier = composed { + this.clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = { }, + ) +} + +fun Modifier.ddBorder( + horizontalSpacing: Dp = 3.dp, + verticalSpacing: Dp = 3.dp, + outline: Shape, + outlineWidth: Dp = 1.dp, + inner: Shape, + innerWidth: Dp = 1.dp, +): Modifier = composed { + val isDarkTheme = isSystemInDarkTheme() + val colorScheme = MaterialTheme.lexicon.colorScheme + + return@composed this + .border( + width = outlineWidth, + color = colorScheme.characterSheet.outlineBorder, + shape = outline, + ) + .padding( + horizontal = horizontalSpacing, + vertical = verticalSpacing, + ) + .border( + width = innerWidth, + color = colorScheme.characterSheet.innerBorder, + shape = inner, + ) + .clip( + shape = inner, + ) + .thenIf( + predicate = { isDarkTheme }, + thenModifier = Modifier.background( + shape = inner, + color = colorScheme.base.surfaceColorAtElevation(2.dp) + ), + ) +} + +inline fun Modifier.thenIf( + crossinline predicate: () -> Boolean, + thenModifier: Modifier? = null, + elseModifier: Modifier? = null, +): Modifier = when (predicate()) { + true -> thenModifier?.let { this.then(it) } ?: this + else -> elseModifier?.let { this.then(it) } ?: this } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/MutableStateEx+Boolean.kt b/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/MutableStateEx+Boolean.kt new file mode 100644 index 0000000..f46ad65 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/MutableStateEx+Boolean.kt @@ -0,0 +1,7 @@ +package com.pixelized.rplexicon.utilitary.extentions + +import androidx.compose.runtime.MutableState + +fun MutableState.switch() { + value = value.not() +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_d10_24.xml b/app/src/main/res/drawable/ic_d10_24.xml new file mode 100644 index 0000000..a62d4ca --- /dev/null +++ b/app/src/main/res/drawable/ic_d10_24.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_d12_24.xml b/app/src/main/res/drawable/ic_d12_24.xml new file mode 100644 index 0000000..dfc2876 --- /dev/null +++ b/app/src/main/res/drawable/ic_d12_24.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_d20_24.xml b/app/src/main/res/drawable/ic_d20_24.xml new file mode 100644 index 0000000..dee702d --- /dev/null +++ b/app/src/main/res/drawable/ic_d20_24.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_d4_24.xml b/app/src/main/res/drawable/ic_d4_24.xml new file mode 100644 index 0000000..c180357 --- /dev/null +++ b/app/src/main/res/drawable/ic_d4_24.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_d6_24.xml b/app/src/main/res/drawable/ic_d6_24.xml new file mode 100644 index 0000000..4086760 --- /dev/null +++ b/app/src/main/res/drawable/ic_d6_24.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_d8_24.xml b/app/src/main/res/drawable/ic_d8_24.xml new file mode 100644 index 0000000..92b87d9 --- /dev/null +++ b/app/src/main/res/drawable/ic_d8_24.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml index 7dc617a..c27306f 100644 --- a/app/src/main/res/drawable/ic_launcher_foreground.xml +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -1,14 +1,15 @@ - + android:viewportWidth="24" + android:viewportHeight="24" + android:tint="@android:color/white"> + + android:fillColor="@android:color/white" + android:pathData="M13.33,.369c-.821-.493-1.843-.494-2.665,0L1,6.141v11.719l9.664,5.771c.411,.247,.873,.37,1.333,.37s.921-.123,1.332-.369l9.671-5.772V6.141L13.33,.369ZM6.315,9.69l-4.315,6.327V7.277l4.315,2.412ZM12.056,1.636l4.094,7.364H7.863L12.056,1.636ZM7.86,10h8.279l-4.141,7.263-4.137-7.263Zm-.877,.481l4.033,7.08-8.336-.767,4.303-6.313Zm10.032,.002l4.411,6.311-8.441,.754,4.03-7.065Zm.664-.79l4.32-2.415V15.87l-4.32-6.178Zm3.82-3.282l-4.309,2.409L13.051,1.368l8.449,5.043ZM11.119,1.262l-4.304,7.561L2.5,6.411,11.119,1.262ZM2.879,17.816l8.618,.786,.002,4.309c-.11-.037-.22-.077-.322-.139L2.879,17.816Zm9.937,4.958c-.1,.06-.208,.099-.316,.135l-.002-4.321,8.607-.762-8.289,4.948Z" /> - + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp index 926ae4f..bcbc62d 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp index 8d4840b..b676d59 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp index 8d205b5..e2b1c8f 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp index 4997a5e..29d31f9 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp index 0eb777f..139d18a 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp index c084387..0a37012 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp index 03aa23c..04bc684 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp index 1b802eb..aee1b6c 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp index 3f5cde9..7b65045 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp index 94ced82..8b85bbe 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 60b9483..789c09d 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -55,4 +55,49 @@ Carte Coordonnées + + Feuille de personnage + Force + FOR + Dextérité + DEX + Constitution + CON + Intelligence + INT + Sagesse + SAG + Charisme + CHA + Jet de sauvegarde + Talent + Acrobaties + Dressage + Arcanes + Athlétisme + Tromperie + Histoire + Intuition + Intimidation + Investigation + Médecine + Nature + Perception + Représentation + Persuasion + Religion + Escamotage + Discrétion + Survie + + Maîtrise \"%1$s\" + Expertise \"%1$s\" + Jet de sauvegarde + + TEST \"%1$s\" + Test \"%1$s\" + Bonus \"%1$s\" + + JET DE SAUVEGARDE : %1$s + Sauvegarde de \"%1$s\" \ No newline at end of file diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml index 0530591..30ecd77 100644 --- a/app/src/main/res/values/ic_launcher_background.xml +++ b/app/src/main/res/values/ic_launcher_background.xml @@ -1,4 +1,4 @@ - #1C1B1F + #00ACC1 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2cc8da1..bf98b5d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -55,4 +55,49 @@ Map Coordinates + + Character sheet + Strength + STR + Dexterity + DEX + Constitution + CON + Intelligence + INT + Wisdom + WIS + Charisma + CHA + Saving Throws + Proficiency + Acrobatics + Animal Handling + Arcana + Athletics + Deception + History + Insight + Intimidation + Investigation + Medicine + Nature + Perception + Performance + Persuasion + Religion + Sleight Of Hand + Stealth + Survival + + %1$s proficiency + %1$s expertise + Saving throw + + %1$s CHECK + %1$s check + %1$s bonus + + %1$s SAVING THROW + %1$s save \ No newline at end of file