Basic implementation of Alteration.

This commit is contained in:
Thomas Andres Gomez 2023-09-10 09:52:51 +02:00
parent 2fb1ef2fd8
commit 6c7ac8e010
51 changed files with 2164 additions and 462 deletions

View file

@ -1,50 +0,0 @@
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() {
private val diceRegex = Regex("(\\d+)d(\\d+)")
private val bonusRegex = Regex("(?:[a-zA-Z]|\\?)[a-zA-Z]+")
fun parseRoll(characterSheet: CharacterSheet, value: String?): Roll {
val roll = value?.split(";")
val label = roll?.getOrNull(0)
val (dices, bonus) = roll?.getOrNull(1)?.let { item ->
val dices = diceRegex.findAll(item).toList().map { it.parseDice() }
val bonus = bonusRegex.findAll(item).map { bonus ->
Roll.Bonus(
label = bonus.value,
value = bonus.value.parseBonus(characterSheet = characterSheet),
)
}
dices.toList() to bonus.toList()
} ?: (null to null)
return Roll(
title = label.toString(),
highlight = null,
dices = dices ?: emptyList(),
bonus = bonus ?: emptyList(),
)
}
private fun String?.parseBonus(
characterSheet: CharacterSheet,
): Int = when (this?.lowercase()) {
"bonus" -> characterSheet.proficiency
"force" -> characterSheet.strength.modifier
else -> 0
}
private fun MatchResult.parseDice(): Roll.Dice {
val (count, faces) = destructured
return Roll.Dice(
count = count.toIntOrNull() ?: 0,
faces = faces.toIntOrNull() ?: 0,
)
}
}

View file

@ -0,0 +1,16 @@
package com.pixelized.rplexicon.facotry.displayable
import com.pixelized.rplexicon.model.Action
import com.pixelized.rplexicon.ui.screens.character.composable.ActionsUio
import com.pixelized.rplexicon.utilitary.extentions.icon
import javax.inject.Inject
class ConvertActionIntoDisplayableFactory @Inject constructor() {
fun toUio(action: Action): ActionsUio {
return ActionsUio(
title = action.title,
hit = action.hit?.faces?.icon,
damage = action.damage?.faces?.icon,
)
}
}

View file

@ -1,4 +1,4 @@
package com.pixelized.rplexicon.facotry
package com.pixelized.rplexicon.facotry.displayable
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.model.CharacterSheet

View file

@ -0,0 +1,13 @@
package com.pixelized.rplexicon.facotry.displayable
import com.pixelized.rplexicon.model.Counter
import com.pixelized.rplexicon.ui.screens.character.composable.CounterUio
import javax.inject.Inject
class ConvertCounterIntoDisplayableFactory @Inject constructor() {
fun toUio(counter: Counter): CounterUio = CounterUio(
title = counter.title,
value = counter.value,
max = counter.max,
)
}

View file

@ -1,4 +1,4 @@
package com.pixelized.rplexicon.facotry
package com.pixelized.rplexicon.facotry.displayable
import com.pixelized.rplexicon.model.Roll
import com.pixelized.rplexicon.ui.screens.rolls.composable.RollDiceUio
@ -44,7 +44,7 @@ class ConvertRollIntoDisplayableFactory @Inject constructor() {
allRolledValues.add(bonus.value)
ThrowsCardUio.Detail(
title = bonus.label,
title = bonus.title,
result = "${bonus.value}",
)
}
@ -64,62 +64,61 @@ class ConvertRollIntoDisplayableFactory @Inject constructor() {
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 fun Roll.Dice.toRollCardUio(result: Int) = RollDiceUio(
icon = faces.icon,
isCriticalSuccess = count == 1 && faces == 20 && result == faces,
isCriticalFailure = count == 1 && faces == 20 && result == 1,
result = "$result",
)
private data class RollResult(
val label: String,
val sum: Int,
)
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 fun Roll.Dice.roll(): RollResult {
return when {
advantage -> {
val roll = List(count) {
(Math.random() * faces + 1).toInt() to (Math.random() * faces + 1).toInt()
private data class RollResult(
val label: String,
val sum: Int,
)
private fun Roll.Dice.roll(): RollResult {
return when {
advantage && !disadvantage -> {
val roll = List(count) { random() to random() }
RollResult(
label = roll.joinToString(" + ") { "${it.first}~${it.second}" },
sum = roll.sumOf { max(it.first, it.second) },
)
}
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()
disadvantage && !advantage -> {
val roll = List(count) { random() to random() }
RollResult(
label = roll.joinToString(" + ") { "${it.first}~${it.second}" },
sum = roll.sumOf { min(it.first, it.second) },
)
}
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()
else -> {
val roll = List(count) { random() }
RollResult(
label = roll.toLabel(),
sum = roll.sum(),
)
}
RollResult(
label = roll.toLabel(),
sum = roll.sum(),
)
}
}
}
private fun Roll.Dice.random(): Int = if (fail) 1 else (Math.random() * faces + 1).toInt()
}

View file

@ -0,0 +1,92 @@
package com.pixelized.rplexicon.facotry.model
import com.google.api.services.sheets.v4.model.ValueRange
import com.pixelized.rplexicon.facotry.model.roll.DiceParser
import com.pixelized.rplexicon.facotry.model.roll.ModifierParser
import com.pixelized.rplexicon.model.Action
import com.pixelized.rplexicon.model.CharacterSheet
import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure
import com.pixelized.rplexicon.utilitary.extentions.checkSheetStructure
import com.pixelized.rplexicon.utilitary.extentions.sheet
import javax.inject.Inject
class ActionParser @Inject constructor(
private val diceParser: DiceParser,
private val modifierParser: ModifierParser,
) {
@Throws(IncompatibleSheetStructure::class)
fun parse(
value: ValueRange,
charactersSheets: Map<String, CharacterSheet>,
): Map<String, List<Action>> {
val sheet = value.values.sheet()
lateinit var structure: Map<String, Int>
val actions = hashMapOf<String, MutableList<Action>>()
sheet?.forEachIndexed { index, row ->
when {
index == 0 -> {
structure = row.checkSheetStructure(model = COLUMNS)
}
row is List<*> -> {
// Assume that the name is the first column.
val characterSheet = charactersSheets[row.getOrNull(0) as? String ?: ""]
val title = row.getOrNull(structure.getValue(NAME))?.toString()
if (characterSheet != null && title != null) {
val action = Action(
title = title,
type = parseType(
value = row.getOrNull(structure.getValue(TYPE))?.toString(),
),
hit = parseThrows(
value = row.getOrNull(structure.getValue(HIT))?.toString(),
),
damage = parseThrows(
value = row.getOrNull(structure.getValue(DAMAGE))?.toString(),
),
)
actions
.getOrPut(characterSheet.name) { mutableListOf() }
.add(action)
}
}
}
}
return actions
}
private fun parseType(value: String?): Action.Type? {
return when (value) {
Action.Type.ATTACK.key -> Action.Type.ATTACK
Action.Type.SPELL.key -> Action.Type.SPELL
else -> null
}
}
private fun parseThrows(value: String?): Action.Throw? {
if (value != null) {
val dice = diceParser.findAll(value = value).firstOrNull()
if (dice != null) {
val modifier = modifierParser.findAll(value = value)
return Action.Throw(
amount = dice.count,
faces = dice.faces,
modifier = modifier,
)
}
}
return null
}
companion object {
const val NAME = "Nom"
const val TYPE = "type"
const val HIT = "Touché"
const val DAMAGE = "Dommage"
val COLUMNS get() = listOf("", NAME, TYPE, HIT, DAMAGE)
}
}

View file

@ -0,0 +1,157 @@
package com.pixelized.rplexicon.facotry.model
import com.google.api.services.sheets.v4.model.ValueRange
import com.pixelized.rplexicon.model.CharacterSheet
import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure
import com.pixelized.rplexicon.utilitary.extentions.checkSheetStructure
import com.pixelized.rplexicon.utilitary.extentions.sheet
import javax.inject.Inject
class CharacterSheetParser @Inject constructor() {
@Throws(IncompatibleSheetStructure::class)
fun parse(value: ValueRange): Map<String, CharacterSheet> {
// fetch the character sheet
val sheet = value.values.sheet() // fetch the List<*>
?.mapNotNull { it as? List<*> } // transform the List<*> to a List<List<*>>
?.let { sheet ->
val entryCount = if (sheet.isNotEmpty()) sheet[0].size else 0
List(entryCount) { index -> sheet.map { it.getOrNull(index) } }
}
// prepare a hash on the structure & function to get index in it.
lateinit var structure: Map<String, Int>
fun List<*>.parse(key: String): Int =
(getOrNull(structure.getValue(key)) as? String)?.toIntOrNull() ?: 0
// parse the sheet
return sheet?.mapIndexedNotNull { index, item ->
when (index) {
0 -> {
structure = item.checkSheetStructure(model = ROWS)
return@mapIndexedNotNull null
}
else -> {
val name = item.getOrNull(structure.getValue(NAME)) as String?
if (name != null) {
CharacterSheet(
name = name,
hitPoint = item.parse(HIT_POINT),
armorClass = item.parse(ARMOR_CLASS),
proficiency = item.parse(MASTERY),
strength = item.parse(STRENGTH),
dexterity = item.parse(DEXTERITY),
constitution = item.parse(CONSTITUTION),
intelligence = item.parse(INTELLIGENCE),
wisdom = item.parse(WISDOM),
charisma = item.parse(CHARISMA),
strengthSavingThrows = item.parse(STRENGTH_SAVING_THROW),
dexteritySavingThrows = item.parse(DEXTERITY_SAVING_THROW),
constitutionSavingThrows = item.parse(CONSTITUTION_SAVING_THROW),
intelligenceSavingThrows = item.parse(INTELLIGENCE_SAVING_THROW),
wisdomSavingThrows = item.parse(WISDOM_SAVING_THROW),
charismaSavingThrows = item.parse(CHARISMA_SAVING_THROW),
acrobatics = item.parse(ACROBATICS),
animalHandling = item.parse(ANIMAL_HANDLING),
arcana = item.parse(ARCANA),
athletics = item.parse(ATHLETICS),
deception = item.parse(DECEPTION),
history = item.parse(HISTORY),
insight = item.parse(INSIGHT),
intimidation = item.parse(INTIMIDATION),
investigation = item.parse(INVESTIGATION),
medicine = item.parse(MEDICINE),
nature = item.parse(NATURE),
perception = item.parse(PERCEPTION),
performance = item.parse(PERFORMANCE),
persuasion = item.parse(PERSUASION),
religion = item.parse(RELIGION),
sleightOfHand = item.parse(SLEIGHT_OF_HAND),
stealth = item.parse(STEALTH),
survival = item.parse(SURVIVAL),
)
} else {
null
}
}
}
}?.associateBy { it.name } ?: emptyMap()
}
companion object {
private const val NAME = "Nom"
private const val HIT_POINT = "Point de vie"
private const val ARMOR_CLASS = "Classe d'armure"
private const val MASTERY = "Bonus de maîtrise"
private const val STRENGTH = "Force"
private const val DEXTERITY = "Dextérité"
private const val CONSTITUTION = "Constitution"
private const val INTELLIGENCE = "Intelligence"
private const val WISDOM = "Sagesse"
private const val CHARISMA = "Charisme"
private const val STRENGTH_SAVING_THROW = "Jet de sauvegarde: Force"
private const val DEXTERITY_SAVING_THROW = "Jet de sauvegarde: Dextérité"
private const val CONSTITUTION_SAVING_THROW = "Jet de sauvegarde: Constitution"
private const val INTELLIGENCE_SAVING_THROW = "Jet de sauvegarde: Intelligence"
private const val WISDOM_SAVING_THROW = "Jet de sauvegarde: Sagesse"
private const val CHARISMA_SAVING_THROW = "Jet de sauvegarde: Charisme"
private const val ACROBATICS = "Acrobaties"
private const val ANIMAL_HANDLING = "Dressage"
private const val ARCANA = "Arcanes"
private const val ATHLETICS = "Athlétisme"
private const val DECEPTION = "Tromperie"
private const val HISTORY = "Histoire"
private const val INSIGHT = "Intuition"
private const val INTIMIDATION = "Intimidation"
private const val INVESTIGATION = "Investigation"
private const val MEDICINE = "Médecine"
private const val NATURE = "Nature"
private const val PERCEPTION = "Perception"
private const val PERFORMANCE = "Représentation"
private const val PERSUASION = "Persuasion"
private const val RELIGION = "Religion"
private const val SLEIGHT_OF_HAND = "Escamotage"
private const val STEALTH = "Discrétion"
private const val SURVIVAL = "Survie"
private val ROWS
get() = listOf(
NAME,
HIT_POINT,
ARMOR_CLASS,
MASTERY,
STRENGTH,
DEXTERITY,
CONSTITUTION,
INTELLIGENCE,
WISDOM,
CHARISMA,
STRENGTH_SAVING_THROW,
DEXTERITY_SAVING_THROW,
CONSTITUTION_SAVING_THROW,
INTELLIGENCE_SAVING_THROW,
WISDOM_SAVING_THROW,
CHARISMA_SAVING_THROW,
ACROBATICS,
ANIMAL_HANDLING,
ARCANA,
ATHLETICS,
DECEPTION,
HISTORY,
INSIGHT,
INTIMIDATION,
INVESTIGATION,
MEDICINE,
NATURE,
PERCEPTION,
PERFORMANCE,
PERSUASION,
RELIGION,
SLEIGHT_OF_HAND,
STEALTH,
SURVIVAL,
)
}
}

View file

@ -1,4 +1,4 @@
package com.pixelized.rplexicon.facotry
package com.pixelized.rplexicon.facotry.model
import com.pixelized.rplexicon.model.Lexicon
import javax.inject.Inject

View file

@ -1,4 +1,4 @@
package com.pixelized.rplexicon.facotry
package com.pixelized.rplexicon.facotry.model
import com.google.api.services.sheets.v4.model.ValueRange
import com.pixelized.rplexicon.model.Lexicon

View file

@ -1,4 +1,4 @@
package com.pixelized.rplexicon.facotry
package com.pixelized.rplexicon.facotry.model
import android.net.Uri
import com.google.api.services.sheets.v4.model.ValueRange

View file

@ -1,4 +1,4 @@
package com.pixelized.rplexicon.facotry
package com.pixelized.rplexicon.facotry.model
import androidx.compose.ui.geometry.Offset
import com.google.api.services.sheets.v4.model.ValueRange

View file

@ -1,4 +1,4 @@
package com.pixelized.rplexicon.facotry
package com.pixelized.rplexicon.facotry.model
import android.net.Uri
import com.pixelized.rplexicon.utilitary.extentions.toUriOrNull

View file

@ -1,4 +1,4 @@
package com.pixelized.rplexicon.facotry
package com.pixelized.rplexicon.facotry.model
import com.google.api.services.sheets.v4.model.ValueRange

View file

@ -1,4 +1,4 @@
package com.pixelized.rplexicon.facotry
package com.pixelized.rplexicon.facotry.model
import com.pixelized.rplexicon.model.Lexicon
import javax.inject.Inject

View file

@ -0,0 +1,133 @@
package com.pixelized.rplexicon.facotry.model.alteration
import com.google.api.services.sheets.v4.model.ValueRange
import com.pixelized.rplexicon.facotry.model.roll.DiceParser
import com.pixelized.rplexicon.facotry.model.roll.FlatValueParser
import com.pixelized.rplexicon.model.Alteration
import com.pixelized.rplexicon.model.Property
import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure
import com.pixelized.rplexicon.utilitary.extentions.checkSheetStructure
import com.pixelized.rplexicon.utilitary.extentions.sheet
import javax.inject.Inject
class AlterationParser @Inject constructor(
private val diceParser: DiceParser,
private val flatParser: FlatValueParser,
private val propertyParser: PropertyParser,
) {
@Throws(IncompatibleSheetStructure::class)
fun parse(value: ValueRange): List<Alteration> {
val sheet = value.values.sheet()
lateinit var structure: Map<String, Int>
return sheet?.mapIndexedNotNull { index, row ->
when {
index == 0 -> {
structure = row.checkSheetStructure(model = COLUMNS)
null
}
row is List<*> -> {
// Assume that the name is the first column.
val name = row.getOrNull(0) as? String
if (name != null) {
Alteration(
name = name,
status = COLUMNS
.mapNotNull { column ->
val property = propertyParser.parseProperty(column)
val flat = row.getOrNull(structure.getValue(column)) as? String
if (property != null && !flat.isNullOrEmpty()) {
property to parseAlterationStatus(name, flat)
} else {
null
}
}
.toMap(),
)
} else {
null
}
}
else -> null
}
} ?: emptyList()
}
private fun parseAlterationStatus(name: String, value: String): Alteration.Status =
when (value) {
"adv" -> Alteration.Status(
name = name,
advantage = true,
disadvantage = false,
fail = false,
)
"dis" -> Alteration.Status(
name = name,
advantage = false,
disadvantage = true,
fail = false,
)
"fail" -> Alteration.Status(
name = name,
advantage = false,
disadvantage = false,
fail = true,
)
else -> {
Alteration.Status(
name = name,
advantage = false,
disadvantage = false,
fail = false,
dices = diceParser.findAll(title = name, value = value),
bonus = flatParser.findAll(title = name, value = value),
)
}
}
companion object {
private val COLUMNS
get() = listOf(
Property.HIT_POINT.key,
Property.ARMOR_CLASS.key,
Property.STRENGTH.key,
Property.DEXTERITY.key,
Property.CONSTITUTION.key,
Property.INTELLIGENCE.key,
Property.WISDOM.key,
Property.CHARISMA.key,
Property.STRENGTH_SAVING_THROW.key,
Property.DEXTERITY_SAVING_THROW.key,
Property.CONSTITUTION_SAVING_THROW.key,
Property.INTELLIGENCE_SAVING_THROW.key,
Property.WISDOM_SAVING_THROW.key,
Property.CHARISMA_SAVING_THROW.key,
Property.ACROBATICS.key,
Property.ANIMAL_HANDLING.key,
Property.ARCANA.key,
Property.ATHLETICS.key,
Property.DECEPTION.key,
Property.HISTORY.key,
Property.INSIGHT.key,
Property.INTIMIDATION.key,
Property.INVESTIGATION.key,
Property.MEDICINE.key,
Property.NATURE.key,
Property.PERCEPTION.key,
Property.PERFORMANCE.key,
Property.PERSUASION.key,
Property.RELIGION.key,
Property.SLEIGHT_OF_HAND.key,
Property.STEALTH.key,
Property.SURVIVAL.key,
Property.ATTACK_ROLL.key,
Property.ATTACK_DAMAGE_ROLL.key,
)
}
}

View file

@ -0,0 +1,60 @@
package com.pixelized.rplexicon.facotry.model.alteration
import com.google.api.services.sheets.v4.model.ValueRange
import com.pixelized.rplexicon.model.Counter
import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure
import com.pixelized.rplexicon.utilitary.extentions.sheet
import javax.inject.Inject
class CounterParser @Inject constructor() {
@Throws(IncompatibleSheetStructure::class)
fun parse(values: ValueRange): Map<String, List<Counter>> {
val sheet = values.values.sheet()
lateinit var characters: List<String>
val counters = hashMapOf<String, MutableList<Counter>>()
sheet?.mapNotNull { it as? List<*> }?.forEachIndexed { columnIndex, row ->
when (columnIndex) {
0 -> characters = row
.subList(fromIndex = 1, toIndex = row.size)
.map { it.toString() }
else -> {
row.getOrNull(0)?.toString()?.let { title ->
row.subList(fromIndex = 1, toIndex = row.size)
.forEachIndexed { rowIndex, value ->
val counter = parseCounter(title = title, value = value?.toString())
if (counter != null) {
counters
.getOrPut(characters[rowIndex]) { mutableListOf() }
.add(counter)
}
}
}
}
}
}
return counters
}
private fun parseCounter(title: String, value: String?): Counter? {
return if (value != null) {
COUNTER_REGEX.find(value)?.let {
val (actual, max) = it.destructured
Counter(
title = title,
value = actual.toIntOrNull() ?: 0,
max = max.toIntOrNull(),
)
}
} else {
null
}
}
companion object {
val COUNTER_REGEX = Regex("(\\d+)\\/(\\d+)")
}
}

View file

@ -0,0 +1,45 @@
package com.pixelized.rplexicon.facotry.model.alteration
import com.pixelized.rplexicon.model.Property
import javax.inject.Inject
class PropertyParser @Inject constructor() {
fun parseProperty(property: String): Property? = when (property) {
Property.PROFICIENCY.key -> Property.PROFICIENCY
Property.HIT_POINT.key -> Property.HIT_POINT
Property.ARMOR_CLASS.key -> Property.ARMOR_CLASS
Property.STRENGTH.key -> Property.STRENGTH
Property.DEXTERITY.key -> Property.DEXTERITY
Property.CONSTITUTION.key -> Property.CONSTITUTION
Property.INTELLIGENCE.key -> Property.INTELLIGENCE
Property.WISDOM.key -> Property.WISDOM
Property.CHARISMA.key -> Property.CHARISMA
Property.STRENGTH_SAVING_THROW.key -> Property.STRENGTH_SAVING_THROW
Property.DEXTERITY_SAVING_THROW.key -> Property.DEXTERITY_SAVING_THROW
Property.CONSTITUTION_SAVING_THROW.key -> Property.CONSTITUTION_SAVING_THROW
Property.INTELLIGENCE_SAVING_THROW.key -> Property.INTELLIGENCE_SAVING_THROW
Property.WISDOM_SAVING_THROW.key -> Property.WISDOM_SAVING_THROW
Property.CHARISMA_SAVING_THROW.key -> Property.CHARISMA_SAVING_THROW
Property.ACROBATICS.key -> Property.ACROBATICS
Property.ANIMAL_HANDLING.key -> Property.ANIMAL_HANDLING
Property.ARCANA.key -> Property.ARCANA
Property.ATHLETICS.key -> Property.ATHLETICS
Property.DECEPTION.key -> Property.DECEPTION
Property.HISTORY.key -> Property.HISTORY
Property.INSIGHT.key -> Property.INSIGHT
Property.INTIMIDATION.key -> Property.INTIMIDATION
Property.INVESTIGATION.key -> Property.INVESTIGATION
Property.MEDICINE.key -> Property.MEDICINE
Property.NATURE.key -> Property.NATURE
Property.PERCEPTION.key -> Property.PERCEPTION
Property.PERFORMANCE.key -> Property.PERFORMANCE
Property.PERSUASION.key -> Property.PERSUASION
Property.RELIGION.key -> Property.RELIGION
Property.SLEIGHT_OF_HAND.key -> Property.SLEIGHT_OF_HAND
Property.STEALTH.key -> Property.STEALTH
Property.SURVIVAL.key -> Property.SURVIVAL
Property.ATTACK_ROLL.key -> Property.ATTACK_ROLL
Property.ATTACK_DAMAGE_ROLL.key -> Property.ATTACK_DAMAGE_ROLL
else -> null
}
}

View file

@ -0,0 +1,43 @@
package com.pixelized.rplexicon.facotry.model.alteration
import com.google.api.services.sheets.v4.model.ValueRange
import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure
import com.pixelized.rplexicon.utilitary.extentions.sheet
import javax.inject.Inject
class StatusParser @Inject constructor() {
@Throws(IncompatibleSheetStructure::class)
fun parse(value: ValueRange): Map<String, List<String>> {
val sheet = value.values.sheet()
lateinit var characters: List<String>
val status = hashMapOf<String, MutableList<String>>()
sheet?.mapNotNull { it as? List<*> }?.forEachIndexed { columnIndex, row ->
when (columnIndex) {
0 -> characters = row
.subList(fromIndex = 1, toIndex = row.size)
.map { it.toString() }
else -> {
row.getOrNull(0)?.toString()?.let { alteration ->
row.subList(fromIndex = 1, toIndex = row.size)
.forEachIndexed { rowIndex, value ->
if (value == TRUE) {
status
.getOrPut(characters[rowIndex]) { mutableListOf() }
.add(alteration)
}
}
}
}
}
}
return status
}
companion object {
private const val TRUE = "TRUE"
}
}

View file

@ -0,0 +1,27 @@
package com.pixelized.rplexicon.facotry.model.roll
import com.pixelized.rplexicon.model.Roll
import javax.inject.Inject
class DiceParser @Inject constructor() {
companion object {
private val DICE_REGEX = Regex("(a|adv|d|dis)*(\\d+)d(\\d+)")
}
fun findAll(title: String? = null, value: String): List<Roll.Dice> =
DICE_REGEX.findAll(value).map { it.parse(title = title) }.toList()
private fun MatchResult.parse(
title: String? = null,
): Roll.Dice {
val (status, count, faces) = destructured
return Roll.Dice(
title = title,
advantage = status == "a" || status == "adv",
disadvantage = status == "d" || status == "dis",
count = count.toIntOrNull() ?: 0,
faces = faces.toIntOrNull() ?: 0,
)
}
}

View file

@ -0,0 +1,24 @@
package com.pixelized.rplexicon.facotry.model.roll
import com.pixelized.rplexicon.model.Roll
import javax.inject.Inject
class FlatValueParser @Inject constructor() {
companion object {
private val FLAT_REGEX = Regex("(?<!d|\\d)(\\d+)(?!d)")
}
fun findAll(title: String, value: String): List<Roll.Bonus> {
return FLAT_REGEX.findAll(value).map { it.parse(title = title) }.toList()
}
private fun MatchResult.parse(
title: String,
): Roll.Bonus {
return Roll.Bonus(
title = title,
value = value.toIntOrNull() ?: 0,
)
}
}

View file

@ -0,0 +1,28 @@
package com.pixelized.rplexicon.facotry.model.roll
import com.pixelized.rplexicon.facotry.model.alteration.PropertyParser
import com.pixelized.rplexicon.model.Property
import javax.inject.Inject
class ModifierParser @Inject constructor(
private val propertyParser: PropertyParser
) {
companion object {
private val MODIFIER_REGEX = Regex(
pattern = Property.PROFICIENCY.key +
"|${Property.STRENGTH.key}" +
"|${Property.DEXTERITY.key}" +
"|${Property.CONSTITUTION.key}" +
"|${Property.INTELLIGENCE.key}" +
"|${Property.WISDOM.key}" +
"|${Property.CHARISMA.key}",
option = RegexOption.IGNORE_CASE
)
}
fun findAll(value: String): List<Property> {
return MODIFIER_REGEX.findAll(value)
.mapNotNull { propertyParser.parseProperty(it.value) }
.toList()
}
}

View file

@ -0,0 +1,19 @@
package com.pixelized.rplexicon.model
data class Action(
val title: String,
val type: Type?,
val hit: Throw?,
val damage: Throw?,
) {
enum class Type(val key: String) {
ATTACK("Attaque"),
SPELL("Sortilège"),
}
class Throw(
val amount: Int,
val faces: Int,
val modifier: List<Property>,
)
}

View file

@ -0,0 +1,15 @@
package com.pixelized.rplexicon.model
data class Alteration(
val name: String,
val status: Map<Property, Status>,
) {
data class Status(
val name: String,
val advantage: Boolean,
val disadvantage: Boolean,
val fail: Boolean,
val dices: List<Roll.Dice> = emptyList(),
val bonus: List<Roll.Bonus> = emptyList(),
)
}

View file

@ -1,6 +1,7 @@
package com.pixelized.rplexicon.model
data class CharacterSheet(
val name: String,
val hitPoint: Int, // Point de vie
val armorClass: Int, // Classe d'armure
val proficiency: Int, // Bonus de maîtrise

View file

@ -0,0 +1,7 @@
package com.pixelized.rplexicon.model
data class Counter(
val title: String,
val value: Int,
val max: Int?,
)

View file

@ -0,0 +1,39 @@
package com.pixelized.rplexicon.model
enum class Property(val key: String) {
PROFICIENCY("Maîtrise"),
HIT_POINT("Point de vie"),
ARMOR_CLASS("Classe d'armure"),
STRENGTH("Force"),
DEXTERITY("Dextérité"),
CONSTITUTION("Constitution"),
INTELLIGENCE("Intelligence"),
WISDOM("Sagesse"),
CHARISMA("Charisme"),
STRENGTH_SAVING_THROW("Jet de sauvegarde: Force"),
DEXTERITY_SAVING_THROW("Jet de sauvegarde: Dextérité"),
CONSTITUTION_SAVING_THROW("Jet de sauvegarde: Constitution"),
INTELLIGENCE_SAVING_THROW("Jet de sauvegarde: Intelligence"),
WISDOM_SAVING_THROW("Jet de sauvegarde: Sagesse"),
CHARISMA_SAVING_THROW("Jet de sauvegarde: Charisme"),
ACROBATICS("Acrobaties"),
ANIMAL_HANDLING("Dressage"),
ARCANA("Arcanes"),
ATHLETICS("Athlétisme"),
DECEPTION("Tromperie"),
HISTORY("Histoire"),
INSIGHT("Intuition"),
INTIMIDATION("Intimidation"),
INVESTIGATION("Investigation"),
MEDICINE("Médecine"),
NATURE("Nature"),
PERCEPTION("Perception"),
PERFORMANCE("Représentation"),
PERSUASION("Persuasion"),
RELIGION("Religion"),
SLEIGHT_OF_HAND("Escamotage"),
STEALTH("Discrétion"),
SURVIVAL("Survie"),
ATTACK_ROLL("Attaque"),
ATTACK_DAMAGE_ROLL("Dommage"),
}

View file

@ -1,108 +1,104 @@
package com.pixelized.rplexicon.model
import androidx.compose.runtime.Stable
@Stable
data class Roll(
val title: String,
val highlight: String? = null,
val dices: List<Dice>,
val bonus: List<Bonus>,
) {
@Stable
data class Dice(
val title: String? = null,
val advantage: Boolean = false,
val disadvantage: Boolean = false,
val fail: 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 title: String,
val value: Int,
)
}
fun d20(
title: String? = null,
advantage: Boolean = false,
disadvantage: Boolean = false,
fail: Boolean = false,
amount: Int = 1,
) = Roll.Dice(
title = title,
advantage = advantage,
disadvantage = disadvantage,
count = amount,
fail = fail,
faces = 20,
)
fun d12(
title: String? = null,
advantage: Boolean = false,
disadvantage: Boolean = false,
amount: Int = 1,
) = Roll.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,
) = Roll.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,
) = Roll.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,
) = Roll.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,
) = Roll.Dice(
title = title,
advantage = advantage,
disadvantage = disadvantage,
count = amount,
faces = 4,
)

View file

@ -0,0 +1,49 @@
package com.pixelized.rplexicon.repository.data
import com.pixelized.rplexicon.facotry.model.ActionParser
import com.pixelized.rplexicon.model.Action
import com.pixelized.rplexicon.repository.GoogleSheetServiceRepository
import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure
import com.pixelized.rplexicon.utilitary.exceptions.ServiceNotReady
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.runInterruptible
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.coroutines.CoroutineContext
@Singleton
class ActionRepository @Inject constructor(
private val googleRepository: GoogleSheetServiceRepository,
private val characterSheetRepository: CharacterSheetRepository,
private val actionParser: ActionParser,
) {
private val _data = MutableStateFlow<Map<String, List<Action>>>(emptyMap())
val data: StateFlow<Map<String, List<Action>>> get() = _data
fun find(name: String?): List<Action>? {
return name?.let { _data.value[it] }
}
@Throws(ServiceNotReady::class, IncompatibleSheetStructure::class, Exception::class)
suspend fun fetchActions() {
googleRepository.fetch { sheet ->
val request = sheet.get(Sheet.ID, Sheet.ACTIONS)
val data = actionParser.parse(
value = request.execute(),
charactersSheets = characterSheetRepository.data.value
)
_data.emit(data)
}
}
private object Sheet {
const val ID = "1fHfzeb8y5u9lEQB1iI-jBEhqu7YSip5sAajXcXK7VJ8"
const val ACTIONS = "Actions"
}
}

View file

@ -0,0 +1,75 @@
package com.pixelized.rplexicon.repository.data
import com.pixelized.rplexicon.facotry.model.alteration.AlterationParser
import com.pixelized.rplexicon.facotry.model.alteration.CounterParser
import com.pixelized.rplexicon.facotry.model.alteration.StatusParser
import com.pixelized.rplexicon.model.Alteration
import com.pixelized.rplexicon.model.Counter
import com.pixelized.rplexicon.model.Property
import com.pixelized.rplexicon.repository.GoogleSheetServiceRepository
import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure
import com.pixelized.rplexicon.utilitary.exceptions.ServiceNotReady
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class AlterationRepository @Inject constructor(
private val googleRepository: GoogleSheetServiceRepository,
private val alterationParser: AlterationParser,
private val statusParser: StatusParser,
private val counterParser: CounterParser,
) {
private val _alterations = MutableStateFlow<List<Alteration>>(emptyList())
val alterations: StateFlow<List<Alteration>> get() = _alterations
private val _status = MutableStateFlow<Map<String, List<String>>>(emptyMap())
val status: StateFlow<Map<String, List<String>>> get() = _status
private val _counter = MutableStateFlow<Map<String, List<Counter>>>(emptyMap())
val counter: StateFlow<Map<String, List<Counter>>> get() = _counter
fun getAlterations(character: String): List<Alteration> {
return status.value[character]?.mapNotNull { alterationName ->
alterations.value.firstOrNull { it.name == alterationName }
} ?: emptyList()
}
fun getStatus(character: String, property: Property): List<Alteration.Status> {
return getAlterations(character).mapNotNull { it.status[property] }
}
fun getCounter(name: String?): List<Counter>? {
return name?.let { counter.value[it] }
}
@Throws(ServiceNotReady::class, IncompatibleSheetStructure::class, Exception::class)
suspend fun fetchAlterationSheet() {
googleRepository.fetch { sheet ->
val request = sheet.get(Sheet.ID, Sheet.ALTERATION_SHEET)
val data = alterationParser.parse(value = request.execute())
_alterations.emit(data)
}
}
@Throws(ServiceNotReady::class, IncompatibleSheetStructure::class, Exception::class)
suspend fun fetchStatusSheet() {
googleRepository.fetch { sheet ->
val request = sheet.get(Sheet.ID, Sheet.STATUS_SHEET)
val status = statusParser.parse(value = request.execute())
_status.emit(status)
val counter = counterParser.parse(values = request.execute())
_counter.emit(counter)
}
}
private object Sheet {
const val ID = "1fHfzeb8y5u9lEQB1iI-jBEhqu7YSip5sAajXcXK7VJ8"
const val ALTERATION_SHEET = "Altérations"
const val STATUS_SHEET = "État des personnages"
const val STATUS_GID = "1246442302"
const val SHEET_URL = "https://docs.google.com/spreadsheets/d/${ID}/edit#gid=$STATUS_GID"
}
}

View file

@ -1,12 +1,10 @@
package com.pixelized.rplexicon.repository.data
import com.google.api.services.sheets.v4.model.ValueRange
import com.pixelized.rplexicon.facotry.RollParser
import com.pixelized.rplexicon.facotry.model.CharacterSheetParser
import com.pixelized.rplexicon.model.CharacterSheet
import com.pixelized.rplexicon.repository.GoogleSheetServiceRepository
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
@ -15,7 +13,7 @@ import javax.inject.Singleton
@Singleton
class CharacterSheetRepository @Inject constructor(
private val googleRepository: GoogleSheetServiceRepository,
private val rollParser: RollParser,
private val characterSheetParser: CharacterSheetParser,
) {
private val _data = MutableStateFlow<Map<String, CharacterSheet>>(emptyMap())
val data: StateFlow<Map<String, CharacterSheet>> get() = _data
@ -28,68 +26,11 @@ class CharacterSheetRepository @Inject constructor(
suspend fun fetchCharacterSheet() {
googleRepository.fetch { sheet ->
val request = sheet.get(Sheet.ID, Sheet.CHARACTER_SHEET)
val data = request.execute()
updateData(data = data)
val data = characterSheetParser.parse(value = request.execute())
_data.emit(data)
}
}
@Throws(IncompatibleSheetStructure::class)
private fun updateData(data: ValueRange?) {
val sheet = data?.values?.sheet()
var id = 0
val bru = sheet?.map { (it as? List<*>)?.get(1) }
val characterSheet = CharacterSheet(
hitPoint = (bru?.get(Sheet.HIT_POINT) as? String)?.toIntOrNull() ?: 0,
armorClass = (bru?.get(Sheet.ARMOR_CLASS) as? String)?.toIntOrNull() ?: 0,
proficiency = (bru?.get(Sheet.PROFICIENCY) as? String)?.toIntOrNull() ?: 0,
strength = (bru?.get(Sheet.STRENGTH) as? String)?.toIntOrNull() ?: 0,
dexterity = (bru?.get(Sheet.DEXTERITY) as? String)?.toIntOrNull() ?: 0,
constitution = (bru?.get(Sheet.CONSTITUTION) as? String)?.toIntOrNull() ?: 0,
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,
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,
athletics = (bru?.get(Sheet.ATHLETICS) as? String)?.toIntOrNull() ?: 0,
deception = (bru?.get(Sheet.DECEPTION) as? String)?.toIntOrNull() ?: 0,
history = (bru?.get(Sheet.HISTORY) as? String)?.toIntOrNull() ?: 0,
insight = (bru?.get(Sheet.INSIGHT) as? String)?.toIntOrNull() ?: 0,
intimidation = (bru?.get(Sheet.INTIMIDATION) as? String)?.toIntOrNull() ?: 0,
investigation = (bru?.get(Sheet.INVESTIGATION) as? String)?.toIntOrNull() ?: 0,
medicine = (bru?.get(Sheet.MEDICINE) as? String)?.toIntOrNull() ?: 0,
nature = (bru?.get(Sheet.NATURE) as? String)?.toIntOrNull() ?: 0,
perception = (bru?.get(Sheet.PERCEPTION) as? String)?.toIntOrNull() ?: 0,
performance = (bru?.get(Sheet.PERFORMANCE) as? String)?.toIntOrNull() ?: 0,
persuasion = (bru?.get(Sheet.PERSUASION) as? String)?.toIntOrNull() ?: 0,
religion = (bru?.get(Sheet.RELIGION) as? String)?.toIntOrNull() ?: 0,
sleightOfHand = (bru?.get(Sheet.SLEIGHT_OF_HAND) as? String)?.toIntOrNull() ?: 0,
stealth = (bru?.get(Sheet.STEALTH) as? String)?.toIntOrNull() ?: 0,
survival = (bru?.get(Sheet.SURVIVAL) as? String)?.toIntOrNull() ?: 0,
)
val rolls = bru?.subList(fromIndex = 34, bru.size)?.mapNotNull {
rollParser.parseRoll(characterSheet = characterSheet, value = it?.toString())
}
_data.tryEmit(
hashMapOf("bru" to characterSheet)
)
}
companion object {
const val TAG = "CharacterSheetRepository"
@ -98,39 +39,5 @@ class CharacterSheetRepository @Inject constructor(
private object Sheet {
const val ID = "1fHfzeb8y5u9lEQB1iI-jBEhqu7YSip5sAajXcXK7VJ8"
const val CHARACTER_SHEET = "Feuille de personnage"
const val HIT_POINT = 1
const val ARMOR_CLASS = 2
const val PROFICIENCY = 3
const val STRENGTH = 4
const val DEXTERITY = 5
const val CONSTITUTION = 6
const val INTELLIGENCE = 7
const val WISDOM = 8
const val CHARISMA = 9
const val STRENGTH_SAVING_THROWS = 10
const val DEXTERITY_SAVING_THROWS = 11
const val CONSTITUTION_SAVING_THROWS = 12
const val INTELLIGENCE_SAVING_THROWS = 13
const val WISDOM_SAVING_THROWS = 14
const val CHARISMA_SAVING_THROWS = 15
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
}
}

View file

@ -1,7 +1,7 @@
package com.pixelized.rplexicon.repository.data
import com.google.api.services.sheets.v4.model.ValueRange
import com.pixelized.rplexicon.facotry.LexiconParser
import com.pixelized.rplexicon.facotry.model.LexiconParser
import com.pixelized.rplexicon.model.Lexicon
import com.pixelized.rplexicon.repository.GoogleSheetServiceRepository
import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure
@ -27,16 +27,11 @@ class LexiconRepository @Inject constructor(
suspend fun fetchLexicon() {
googleRepository.fetch { sheet ->
val request = sheet.get(Sheet.ID, Sheet.LEXICON)
val data = request.execute()
updateData(data = data)
val data = lexiconParser.parse(data = request.execute())
_data.tryEmit(data)
}
}
@Throws(IncompatibleSheetStructure::class)
private fun updateData(data: ValueRange) {
val lexicon = lexiconParser.parse(data)
_data.tryEmit(lexicon)
}
companion object {
const val TAG = "LexiconRepository"

View file

@ -2,8 +2,8 @@ package com.pixelized.rplexicon.repository.data
import com.google.api.services.sheets.v4.Sheets
import com.google.api.services.sheets.v4.model.ValueRange
import com.pixelized.rplexicon.facotry.LocationParser
import com.pixelized.rplexicon.facotry.MarqueeParser
import com.pixelized.rplexicon.facotry.model.LocationParser
import com.pixelized.rplexicon.facotry.model.MarqueeParser
import com.pixelized.rplexicon.model.Location
import com.pixelized.rplexicon.repository.GoogleSheetServiceRepository
import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure
@ -40,7 +40,7 @@ class LocationRepository @Inject constructor(
}
@Throws(IncompatibleSheetStructure::class)
private fun updateData(map: ValueRange, marquee: ValueRange) {
private suspend fun updateData(map: ValueRange, marquee: ValueRange) {
val marquees = marqueeParser
.parse(data = marquee)
.groupBy { it.map }
@ -56,7 +56,7 @@ class LocationRepository @Inject constructor(
}
}
_data.tryEmit(maps)
_data.emit(maps)
}
companion object {

View file

@ -1,7 +1,7 @@
package com.pixelized.rplexicon.repository.data
import com.google.api.services.sheets.v4.model.ValueRange
import com.pixelized.rplexicon.facotry.QuestParser
import com.pixelized.rplexicon.facotry.model.QuestParser
import com.pixelized.rplexicon.model.Quest
import com.pixelized.rplexicon.repository.GoogleSheetServiceRepository
import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure
@ -29,7 +29,7 @@ class QuestRepository @Inject constructor(
}
@Throws(IncompatibleSheetStructure::class, Exception::class)
private fun updateData(data: ValueRange) {
private suspend fun updateData(data: ValueRange) {
val questEntries = questParser.parse(value = data)
val questMap = questEntries.groupBy { it.title }
@ -40,6 +40,6 @@ class QuestRepository @Inject constructor(
entries = questMap[item] ?: emptyList(),
)
}
_data.tryEmit(quests)
_data.emit(quests)
}
}

View file

@ -1,5 +1,8 @@
package com.pixelized.rplexicon.ui.composable
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.ExperimentalMaterialApi
@ -18,7 +21,12 @@ fun Loader(
refreshState: PullRefreshState,
refreshing: State<Boolean>,
) {
if (refreshing.value) {
AnimatedVisibility(
modifier = modifier.fillMaxWidth(),
visible = refreshing.value,
enter = fadeIn(),
exit = fadeOut(),
) {
LinearProgressIndicator(
modifier = modifier
.fillMaxWidth()

View file

@ -1,27 +1,51 @@
package com.pixelized.rplexicon.ui.navigation.screens
import androidx.compose.runtime.Stable
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.NavOptionsBuilder
import androidx.navigation.NavType
import androidx.navigation.navArgument
import com.pixelized.rplexicon.ui.navigation.NavigationAnimation
import com.pixelized.rplexicon.ui.navigation.animatedComposable
import com.pixelized.rplexicon.ui.screens.character.CharacterSheetScreen
import com.pixelized.rplexicon.utilitary.extentions.ARG
private const val ROUTE = "characterSheet"
const val CHARACTER_SHEET_ROUTE = ROUTE
private const val CHARACTER_SHEET_NAME = "id"
val CHARACTER_SHEET_ROUTE = "$ROUTE?${CHARACTER_SHEET_NAME.ARG}"
@Stable
data class CharacterSheetArgument(
val name: String,
)
val SavedStateHandle.characterSheetArgument
get() = CharacterSheetArgument(
name = get(CHARACTER_SHEET_NAME)
?: error("CharacterDetailArgument argument: $CHARACTER_SHEET_NAME"),
)
fun NavGraphBuilder.composableCharacterSheet() {
animatedComposable(
route = CHARACTER_SHEET_ROUTE,
animation = NavigationAnimation.Push,
arguments = listOf(
navArgument(name = CHARACTER_SHEET_NAME) {
type = NavType.StringType
nullable = false
}
),
) {
CharacterSheetScreen()
}
}
fun NavHostController.navigateToCharacterSheet(
name: String,
option: NavOptionsBuilder.() -> Unit = {},
) {
val route = ROUTE
val route = "$ROUTE?$CHARACTER_SHEET_NAME=$name"
navigate(route = route, builder = option)
}

View file

@ -52,6 +52,7 @@ import com.pixelized.rplexicon.LocalActivity
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost
import com.pixelized.rplexicon.ui.navigation.rootOption
import com.pixelized.rplexicon.ui.navigation.screens.navigateToCharacterSheet
import com.pixelized.rplexicon.ui.navigation.screens.navigateToHome
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.ui.theme.colors.GoogleColorPalette

View file

@ -2,37 +2,62 @@ package com.pixelized.rplexicon.ui.screens.character
import android.content.res.Configuration
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.Image
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CutCornerShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.pullrefresh.PullRefreshState
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
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.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
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.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.composable.Loader
import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost
import com.pixelized.rplexicon.ui.screens.character.composable.Action
import com.pixelized.rplexicon.ui.screens.character.composable.ActionsUio
import com.pixelized.rplexicon.ui.screens.character.composable.Counter
import com.pixelized.rplexicon.ui.screens.character.composable.CounterUio
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
@ -40,6 +65,8 @@ 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
import com.pixelized.rplexicon.utilitary.extentions.ddBorder
import kotlinx.coroutines.launch
@Stable
data class CharacterSheetUio(
@ -75,14 +102,21 @@ data class CharacterSheetUio(
val survival: ProficiencyUio,
)
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun CharacterSheetScreen(
viewModel: CharacterSheetViewModel = hiltViewModel(),
) {
val screen = LocalScreenNavHost.current
val overlay = LocalRollOverlay.current
val scope = rememberCoroutineScope()
Surface(
val refresh = rememberPullRefreshState(
refreshing = false,
onRefresh = { scope.launch { viewModel.update() } },
)
Box(
modifier = Modifier.fillMaxSize(),
) {
viewModel.sheet.value?.let {
@ -91,7 +125,13 @@ fun CharacterSheetScreen(
.fillMaxSize()
.systemBarsPadding(),
state = rememberScrollState(),
refreshState = refresh,
refreshing = viewModel.isLoading,
onRefresh = { scope.launch { viewModel.update() } },
sheet = it,
actions = viewModel.actions,
counter = viewModel.counter,
alterations = viewModel.alterations,
onBack = {
screen.popBackStack()
},
@ -245,6 +285,16 @@ fun CharacterSheetScreen(
overlay.prepareRoll(roll = roll)
overlay.showOverlay()
},
onHit = { id ->
val roll = viewModel.onHitRoll(id)
overlay.prepareRoll(roll = roll)
overlay.showOverlay()
},
onDamage = { id ->
val roll = viewModel.onDamageRoll(id)
overlay.prepareRoll(roll = roll)
overlay.showOverlay()
}
)
}
@ -254,12 +304,18 @@ fun CharacterSheetScreen(
}
}
@OptIn(ExperimentalMaterial3Api::class)
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
@Composable
private fun CharacterSheetContent(
modifier: Modifier = Modifier,
state: ScrollState,
refreshState: PullRefreshState,
refreshing: State<Boolean>,
onRefresh: () -> Unit,
sheet: CharacterSheetUio,
actions: State<List<ActionsUio>>,
counter: State<List<CounterUio>>,
alterations: State<List<String>>,
onBack: () -> Unit,
onStrength: () -> Unit,
onDexterity: () -> Unit,
@ -291,6 +347,8 @@ private fun CharacterSheetContent(
onSleightOfHand: () -> Unit,
onStealth: () -> Unit,
onSurvival: () -> Unit,
onHit: (id: String) -> Unit,
onDamage: (id: String) -> Unit,
) {
Scaffold(
modifier = modifier,
@ -306,6 +364,14 @@ private fun CharacterSheetContent(
)
}
},
actions = {
IconButton(onClick = onRefresh) {
Icon(
imageVector = Icons.Default.Refresh,
contentDescription = null,
)
}
},
title = {
Text(
text = stringResource(id = R.string.character_sheet_title),
@ -316,6 +382,7 @@ private fun CharacterSheetContent(
) { paddingValues ->
Surface(
modifier = Modifier
.pullRefresh(refreshState)
.verticalScroll(state = state)
.padding(paddingValues = paddingValues),
) {
@ -367,6 +434,41 @@ private fun CharacterSheetContent(
}
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) {
Image(
modifier = Modifier.graphicsLayer { rotationY = 180f },
painter = painterResource(id = R.drawable.art_clip_1),
contentScale = ContentScale.FillWidth,
alignment = Alignment.Center,
colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.onSurface),
contentDescription = null,
)
Text(
modifier = Modifier
.weight(weight = 1f, fill = false)
.padding(horizontal = 8.dp),
style = MaterialTheme.typography.titleLarge,
textAlign = TextAlign.Center,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = stringResource(R.string.character_sheet_title_saving_throws),
)
Image(
painter = painterResource(id = R.drawable.art_clip_1),
contentScale = ContentScale.FillWidth,
alignment = Alignment.Center,
colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.onSurface),
contentDescription = null,
)
}
Row(
modifier = Modifier.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
@ -407,88 +509,250 @@ private fun CharacterSheetContent(
}
}
Column {
Proficiency(
modifier = Modifier.clickable(onClick = onAcrobatics),
proficiency = sheet.acrobatics,
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) {
Image(
modifier = Modifier.graphicsLayer { rotationY = 180f },
painter = painterResource(id = R.drawable.art_clip_1),
contentScale = ContentScale.FillWidth,
alignment = Alignment.Center,
colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.onSurface),
contentDescription = null,
)
Proficiency(
modifier = Modifier.clickable(onClick = onAnimalHandling),
proficiency = sheet.animalHandling,
Text(
modifier = Modifier
.weight(weight = 1f, fill = false)
.padding(horizontal = 8.dp),
style = MaterialTheme.typography.titleLarge,
textAlign = TextAlign.Center,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = stringResource(R.string.character_sheet_title_proficiencies),
)
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,
Image(
painter = painterResource(id = R.drawable.art_clip_1),
contentScale = ContentScale.FillWidth,
alignment = Alignment.Center,
colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.onSurface),
contentDescription = null,
)
}
Column(
modifier = Modifier
.padding(horizontal = 16.dp)
.ddBorder(
inner = remember { RoundedCornerShape(size = 8.dp) },
outline = remember { CutCornerShape(size = 16.dp) },
),
) {
Proficiency(
proficiency = sheet.acrobatics,
onClick = onAcrobatics,
)
Proficiency(
proficiency = sheet.animalHandling,
onClick = onAnimalHandling,
)
Proficiency(
proficiency = sheet.arcana,
onClick = onArcana,
)
Proficiency(
proficiency = sheet.athletics,
onClick = onAthletics,
)
Proficiency(
proficiency = sheet.deception,
onClick = onDeception,
)
Proficiency(
proficiency = sheet.history,
onClick = onHistory,
)
Proficiency(
proficiency = sheet.insight,
onClick = onInsight,
)
Proficiency(
proficiency = sheet.intimidation,
onClick = onIntimidation,
)
Proficiency(
proficiency = sheet.investigation,
onClick = onInvestigation,
)
Proficiency(
proficiency = sheet.medicine,
onClick = onMedicine,
)
Proficiency(
proficiency = sheet.nature,
onClick = onNature,
)
Proficiency(
proficiency = sheet.perception,
onClick = onPerception,
)
Proficiency(
proficiency = sheet.performance,
onClick = onPerformance,
)
Proficiency(
proficiency = sheet.persuasion,
onClick = onPersuasion,
)
Proficiency(
proficiency = sheet.religion,
onClick = onReligion,
)
Proficiency(
proficiency = sheet.sleightOfHand,
onClick = onSleightOfHand,
)
Proficiency(
proficiency = sheet.stealth,
onClick = onStealth,
)
Proficiency(
proficiency = sheet.survival,
onClick = onSurvival,
)
}
if (actions.value.isNotEmpty() || counter.value.isNotEmpty()) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) {
Image(
modifier = Modifier.graphicsLayer { rotationY = 180f },
painter = painterResource(id = R.drawable.art_clip_1),
contentScale = ContentScale.FillWidth,
alignment = Alignment.Center,
colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.onSurface),
contentDescription = null,
)
Text(
modifier = Modifier
.weight(weight = 1f, fill = false)
.padding(horizontal = 8.dp),
style = MaterialTheme.typography.titleLarge,
textAlign = TextAlign.Center,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = stringResource(R.string.character_sheet_title_actions),
)
Image(
painter = painterResource(id = R.drawable.art_clip_1),
contentScale = ContentScale.FillWidth,
alignment = Alignment.Center,
colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.onSurface),
contentDescription = null,
)
}
Column(
modifier = Modifier.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
counter.value.forEach {
Counter(
counter = it,
)
}
actions.value.forEach {
Action(
action = it,
onHit = onHit,
onDamage = onDamage,
)
}
}
}
if (alterations.value.isNotEmpty()) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) {
Image(
modifier = Modifier.graphicsLayer { rotationY = 180f },
painter = painterResource(id = R.drawable.art_clip_1),
contentScale = ContentScale.FillWidth,
alignment = Alignment.Center,
colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.onSurface),
contentDescription = null,
)
Text(
modifier = Modifier
.weight(weight = 1f, fill = false)
.padding(horizontal = 8.dp),
style = MaterialTheme.typography.titleLarge,
textAlign = TextAlign.Center,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = stringResource(R.string.character_sheet_title_alteration),
)
Image(
painter = painterResource(id = R.drawable.art_clip_1),
contentScale = ContentScale.FillWidth,
alignment = Alignment.Center,
colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.onSurface),
contentDescription = null,
)
}
Column(
modifier = Modifier
.padding(horizontal = 16.dp)
.fillMaxWidth()
.ddBorder(
inner = remember { RoundedCornerShape(size = 8.dp) },
outline = remember { CutCornerShape(size = 16.dp) },
)
.padding(all = 16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
alterations.value.forEach {
Text(
text = it
)
}
}
}
}
}
Box(
modifier = Modifier
.padding(paddingValues = paddingValues)
.fillMaxWidth(),
) {
Loader(
modifier = Modifier.align(Alignment.TopCenter),
refreshState = refreshState,
refreshing = refreshing,
)
}
}
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO, heightDp = 2000)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, heightDp = 2000)
private fun CharacterScreenPreview() {
LexiconTheme {
val bru = remember {
@ -668,7 +932,40 @@ private fun CharacterScreenPreview() {
CharacterSheetContent(
modifier = Modifier.fillMaxSize(),
state = rememberScrollState(),
refreshState = rememberPullRefreshState(refreshing = false, onRefresh = { }),
refreshing = remember { mutableStateOf(false) },
onRefresh = { },
sheet = bru,
actions = remember {
mutableStateOf(
listOf(
ActionsUio(
title = "Battle Axe",
hit = R.drawable.ic_d20_24,
damage = R.drawable.ic_d8_24,
),
ActionsUio(
title = "Greataxe",
hit = R.drawable.ic_d20_24,
damage = R.drawable.ic_d12_24,
),
)
)
},
counter = remember {
mutableStateOf(
listOf(
CounterUio(
title = "Rage",
value = 1,
max = 2,
),
)
)
},
alterations = remember {
mutableStateOf(listOf("Rage", "Attaque téméraire"))
},
onBack = { },
onStrength = { },
onDexterity = { },
@ -700,6 +997,8 @@ private fun CharacterScreenPreview() {
onSleightOfHand = { },
onStealth = { },
onSurvival = { },
onHit = { },
onDamage = { },
)
}
}

View file

@ -1,107 +1,231 @@
package com.pixelized.rplexicon.ui.screens.character
import android.app.Application
import android.util.Log
import androidx.annotation.StringRes
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.facotry.ConvertCharacterSheetIntoDisplayableFactory
import com.pixelized.rplexicon.facotry.displayable.ConvertActionIntoDisplayableFactory
import com.pixelized.rplexicon.facotry.displayable.ConvertCharacterSheetIntoDisplayableFactory
import com.pixelized.rplexicon.facotry.displayable.ConvertCounterIntoDisplayableFactory
import com.pixelized.rplexicon.model.Action
import com.pixelized.rplexicon.model.CharacterSheet
import com.pixelized.rplexicon.model.Property
import com.pixelized.rplexicon.model.Roll
import com.pixelized.rplexicon.model.d20
import com.pixelized.rplexicon.repository.data.ActionRepository
import com.pixelized.rplexicon.repository.data.AlterationRepository
import com.pixelized.rplexicon.repository.data.CharacterSheetRepository
import com.pixelized.rplexicon.ui.navigation.screens.characterSheetArgument
import com.pixelized.rplexicon.ui.screens.character.composable.ActionsUio
import com.pixelized.rplexicon.ui.screens.character.composable.CounterUio
import com.pixelized.rplexicon.utilitary.extentions.context
import com.pixelized.rplexicon.utilitary.extentions.modifier
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class CharacterSheetViewModel @Inject constructor(
application: Application,
repository: CharacterSheetRepository,
factory: ConvertCharacterSheetIntoDisplayableFactory,
savedStateHandle: SavedStateHandle,
private val characterSheetFactory: ConvertCharacterSheetIntoDisplayableFactory,
private val counterFactory: ConvertCounterIntoDisplayableFactory,
private val actionFactory: ConvertActionIntoDisplayableFactory,
private val characterRepository: CharacterSheetRepository,
private val alterationRepository: AlterationRepository,
private val actionRepository: ActionRepository,
) : AndroidViewModel(application = application) {
private val argument = savedStateHandle.characterSheetArgument
private lateinit var model: CharacterSheet
private val _sheet = mutableStateOf<CharacterSheetUio?>(null)
val sheet: State<CharacterSheetUio?> get() = _sheet
val sheet: State<CharacterSheetUio?>
val alterations: State<List<String>>
val actions: State<List<ActionsUio>>
val counter: State<List<CounterUio>>
private val _isLoading = mutableStateOf(false)
val isLoading: State<Boolean> get() = _isLoading
init {
viewModelScope.launch {
repository.fetchCharacterSheet()
_sheet.value = repository.find("bru")?.let {
sheet = mutableStateOf(
characterRepository.find(name = argument.name)?.let {
model = it
factory.toUio(sheet = it)
characterSheetFactory.toUio(sheet = model)
}
)
alterations = mutableStateOf(
alterationRepository.getAlterations(argument.name).map {
it.name
}
)
counter = mutableStateOf(
alterationRepository.getCounter(argument.name)?.map {
counterFactory.toUio(counter = it)
} ?: emptyList()
)
actions = mutableStateOf(
actionRepository.find(name = argument.name)?.map {
actionFactory.toUio(action = it)
} ?: emptyList()
)
viewModelScope.launch {
launch {
characterRepository.data.collect {
model = it.getValue(key = argument.name)
sheet.value = characterSheetFactory.toUio(sheet = model)
}
}
launch {
actionRepository.data.collect {
actions.value = it[argument.name]?.map { action ->
actionFactory.toUio(action = action)
} ?: emptyList()
}
}
launch {
alterationRepository.counter.collect {
counter.value = it[argument.name]?.map { counter ->
counterFactory.toUio(counter = counter)
} ?: emptyList()
}
}
launch {
alterationRepository.status.collect {
alterations.value = it[argument.name]?.map { name -> name } ?: emptyList()
}
}
}
}
suspend fun update() = coroutineScope {
_isLoading.value = true
val characterRequest = async {
try {
characterRepository.fetchCharacterSheet()
actionRepository.fetchActions()
} catch (exception: Exception) {
Log.e(TAG, exception.message, exception)
}
}
val statusRequest = async {
try {
alterationRepository.fetchStatusSheet()
} catch (exception: Exception) {
Log.e(TAG, exception.message, exception)
}
}
val alterationRequest = async {
try {
alterationRepository.fetchAlterationSheet()
} catch (exception: Exception) {
Log.e(TAG, exception.message, exception)
}
}
awaitAll(characterRequest, statusRequest, alterationRequest)
_isLoading.value = false
}
fun onHitRoll(id: String): Roll {
val action = actionRepository.find(argument.name)?.firstOrNull { it.title == id }
return actionRoll(
action = action,
throws = action?.hit,
)
}
fun onDamageRoll(id: String): Roll {
val action = actionRepository.find(argument.name)?.firstOrNull { it.title == id }
return actionRoll(
action = action,
throws = action?.damage,
)
}
fun strengthRoll(): Roll = statRoll(
abilityRes = R.string.character_sheet_stat_strength,
abilityValue = model.strength,
property = Property.STRENGTH,
)
fun dexterityRoll(): Roll = statRoll(
abilityRes = R.string.character_sheet_stat_dexterity,
abilityValue = model.dexterity,
property = Property.DEXTERITY,
)
fun constitutionRoll(): Roll = statRoll(
abilityRes = R.string.character_sheet_stat_constitution,
abilityValue = model.constitution,
property = Property.CONSTITUTION,
)
fun intelligenceRoll(): Roll = statRoll(
abilityRes = R.string.character_sheet_stat_intelligence,
abilityValue = model.intelligence,
property = Property.INTELLIGENCE,
)
fun wisdomRoll(): Roll = statRoll(
abilityRes = R.string.character_sheet_stat_wisdom,
abilityValue = model.wisdom,
property = Property.WISDOM,
)
fun charismaRoll(): Roll = statRoll(
abilityRes = R.string.character_sheet_stat_charisma,
abilityValue = model.charisma,
property = Property.CHARISMA,
)
fun strengthSavingThrowsRoll(): Roll = savingThrowRoll(
abilityRes = R.string.character_sheet_stat_strength,
abilityValue = model.strength,
masteryLevel = model.strengthSavingThrows,
property = Property.STRENGTH_SAVING_THROW,
)
fun dexteritySavingThrowsRoll(): Roll = savingThrowRoll(
abilityRes = R.string.character_sheet_stat_dexterity,
abilityValue = model.dexterity,
masteryLevel = model.dexteritySavingThrows,
property = Property.DEXTERITY_SAVING_THROW,
)
fun constitutionSavingThrowsRoll(): Roll = savingThrowRoll(
abilityRes = R.string.character_sheet_stat_constitution,
abilityValue = model.constitution,
masteryLevel = model.constitutionSavingThrows,
property = Property.CONSTITUTION_SAVING_THROW,
)
fun intelligenceSavingThrowsRoll(): Roll = savingThrowRoll(
abilityRes = R.string.character_sheet_stat_intelligence,
abilityValue = model.intelligence,
masteryLevel = model.intelligenceSavingThrows,
property = Property.INTELLIGENCE_SAVING_THROW,
)
fun wisdomSavingThrowsRoll(): Roll = savingThrowRoll(
abilityRes = R.string.character_sheet_stat_wisdom,
abilityValue = model.wisdom,
masteryLevel = model.wisdomSavingThrows,
property = Property.WISDOM_SAVING_THROW,
)
fun charismaSavingThrowsRoll(): Roll = savingThrowRoll(
abilityRes = R.string.character_sheet_stat_charisma,
abilityValue = model.charisma,
masteryLevel = model.charismaSavingThrows,
property = Property.CHARISMA_SAVING_THROW,
)
fun acrobaticsRoll(): Roll = abilityRoll(
@ -109,6 +233,7 @@ class CharacterSheetViewModel @Inject constructor(
relatedRes = R.string.character_sheet_stat_dexterity,
abilityValue = model.dexterity,
masteryLevel = model.acrobatics,
property = Property.ACROBATICS,
)
fun animalHandlingRoll(): Roll = abilityRoll(
@ -116,6 +241,7 @@ class CharacterSheetViewModel @Inject constructor(
relatedRes = R.string.character_sheet_stat_wisdom,
abilityValue = model.wisdom,
masteryLevel = model.animalHandling,
property = Property.ANIMAL_HANDLING,
)
fun arcanaRoll(): Roll = abilityRoll(
@ -123,6 +249,7 @@ class CharacterSheetViewModel @Inject constructor(
relatedRes = R.string.character_sheet_stat_intelligence,
abilityValue = model.intelligence,
masteryLevel = model.arcana,
property = Property.ARCANA,
)
fun athleticsRoll(): Roll = abilityRoll(
@ -130,6 +257,7 @@ class CharacterSheetViewModel @Inject constructor(
relatedRes = R.string.character_sheet_stat_strength,
abilityValue = model.strength,
masteryLevel = model.athletics,
property = Property.ATHLETICS,
)
fun deceptionRoll(): Roll = abilityRoll(
@ -137,6 +265,7 @@ class CharacterSheetViewModel @Inject constructor(
relatedRes = R.string.character_sheet_stat_charisma,
abilityValue = model.charisma,
masteryLevel = model.deception,
property = Property.DECEPTION,
)
fun historyRoll(): Roll = abilityRoll(
@ -144,6 +273,7 @@ class CharacterSheetViewModel @Inject constructor(
relatedRes = R.string.character_sheet_stat_intelligence,
abilityValue = model.intelligence,
masteryLevel = model.history,
property = Property.HISTORY,
)
fun insightRoll(): Roll = abilityRoll(
@ -151,6 +281,7 @@ class CharacterSheetViewModel @Inject constructor(
relatedRes = R.string.character_sheet_stat_wisdom,
abilityValue = model.wisdom,
masteryLevel = model.insight,
property = Property.INSIGHT,
)
fun intimidationRoll(): Roll = abilityRoll(
@ -158,6 +289,7 @@ class CharacterSheetViewModel @Inject constructor(
relatedRes = R.string.character_sheet_stat_charisma,
abilityValue = model.charisma,
masteryLevel = model.intimidation,
property = Property.INTIMIDATION,
)
fun investigationRoll(): Roll = abilityRoll(
@ -165,6 +297,7 @@ class CharacterSheetViewModel @Inject constructor(
relatedRes = R.string.character_sheet_stat_intelligence,
abilityValue = model.intelligence,
masteryLevel = model.investigation,
property = Property.INVESTIGATION,
)
fun medicineRoll(): Roll = abilityRoll(
@ -172,6 +305,7 @@ class CharacterSheetViewModel @Inject constructor(
relatedRes = R.string.character_sheet_stat_wisdom,
abilityValue = model.wisdom,
masteryLevel = model.medicine,
property = Property.MEDICINE,
)
fun natureRoll(): Roll = abilityRoll(
@ -179,6 +313,7 @@ class CharacterSheetViewModel @Inject constructor(
relatedRes = R.string.character_sheet_stat_intelligence,
abilityValue = model.intelligence,
masteryLevel = model.nature,
property = Property.NATURE,
)
fun perceptionRoll(): Roll = abilityRoll(
@ -186,6 +321,7 @@ class CharacterSheetViewModel @Inject constructor(
relatedRes = R.string.character_sheet_stat_wisdom,
abilityValue = model.wisdom,
masteryLevel = model.perception,
property = Property.PERCEPTION,
)
fun performanceRoll(): Roll = abilityRoll(
@ -193,6 +329,7 @@ class CharacterSheetViewModel @Inject constructor(
relatedRes = R.string.character_sheet_stat_charisma,
abilityValue = model.charisma,
masteryLevel = model.performance,
property = Property.PERFORMANCE,
)
fun persuasionRoll(): Roll = abilityRoll(
@ -200,6 +337,7 @@ class CharacterSheetViewModel @Inject constructor(
relatedRes = R.string.character_sheet_stat_charisma,
abilityValue = model.charisma,
masteryLevel = model.persuasion,
property = Property.PERSUASION,
)
fun religionRoll(): Roll = abilityRoll(
@ -207,6 +345,7 @@ class CharacterSheetViewModel @Inject constructor(
relatedRes = R.string.character_sheet_stat_intelligence,
abilityValue = model.intelligence,
masteryLevel = model.religion,
property = Property.RELIGION,
)
fun sleightOfHandRoll(): Roll = abilityRoll(
@ -214,6 +353,7 @@ class CharacterSheetViewModel @Inject constructor(
relatedRes = R.string.character_sheet_stat_dexterity,
abilityValue = model.dexterity,
masteryLevel = model.sleightOfHand,
property = Property.SLEIGHT_OF_HAND,
)
fun stealthRoll(): Roll = abilityRoll(
@ -221,6 +361,7 @@ class CharacterSheetViewModel @Inject constructor(
relatedRes = R.string.character_sheet_stat_dexterity,
abilityValue = model.dexterity,
masteryLevel = model.stealth,
property = Property.STEALTH,
)
fun survivalRoll(): Roll = abilityRoll(
@ -228,30 +369,45 @@ class CharacterSheetViewModel @Inject constructor(
relatedRes = R.string.character_sheet_stat_wisdom,
abilityValue = model.wisdom,
masteryLevel = model.survival,
property = Property.SURVIVAL,
)
// //////////////////////////////////////
// region: Helpers
////////////////////////////////////////
// region: Helpers
private fun statRoll(
abilityRes: Int,
abilityValue: Int,
property: Property,
): Roll {
val ability = context.getString(abilityRes)
// get the alteration for a given player
val alterations = alterationRepository.getStatus(
character = model.name,
property = property,
)
// check if any alteration give us advantage or disadvantage
val advantage = alterations.any { it.advantage }
val disadvantage = alterations.any { it.disadvantage }
val fail = alterations.any { it.fail }
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)
d20(
title = context.getString(R.string.dice_roll_check_detail, ability),
advantage = advantage,
disadvantage = disadvantage,
fail = fail,
),
),
) + alterations.map { it.dices }.flatten(),
bonus = listOf(
Roll.Bonus(
label = context.getString(R.string.dice_roll_bonus_detail, ability),
title = context.getString(R.string.dice_roll_bonus_detail, ability),
value = abilityValue.modifier
)
),
) + alterations.map { it.bonus }.flatten(),
)
}
@ -260,29 +416,44 @@ class CharacterSheetViewModel @Inject constructor(
abilityValue: Int,
masteryLevel: Int,
masteryValue: Int = model.proficiency,
property: Property,
): Roll {
val ability = context.getString(abilityRes)
// get the alteration for a given player
val alterations = alterationRepository.getStatus(
character = model.name,
property = property,
)
// check if any alteration give us advantage or disadvantage
val advantage = alterations.any { it.advantage }
val disadvantage = alterations.any { it.disadvantage }
val fail = alterations.any { it.fail }
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)
d20(
title = context.getString(R.string.dice_roll_saving_throw_detail, ability),
advantage = advantage,
disadvantage = disadvantage,
fail = fail,
),
),
) + alterations.map { it.dices }.flatten(),
bonus = listOf(
Roll.Bonus(
label = context.getString(
title = 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),
title = context.getString(R.string.dice_roll_bonus_detail, ability),
value = abilityValue.modifier
)
),
),
) + alterations.map { it.bonus }.flatten(),
)
}
@ -292,30 +463,166 @@ class CharacterSheetViewModel @Inject constructor(
abilityValue: Int,
masteryLevel: Int,
masteryValue: Int = model.proficiency,
property: Property,
): Roll {
val ability = context.getString(abilityRes)
val related = context.getString(relatedRes)
// get the alteration for a given player
val alterations = alterationRepository.getStatus(
character = model.name,
property = property,
)
// check if any alteration give us advantage or disadvantage
val advantage = alterations.any { it.advantage }
val disadvantage = alterations.any { it.disadvantage }
val fail = alterations.any { it.fail }
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)
d20(
title = context.getString(R.string.dice_roll_check_detail, ability),
advantage = advantage,
disadvantage = disadvantage,
fail = fail,
),
),
) + alterations.map { it.dices }.flatten(),
bonus = listOf(
Roll.Bonus(
label = context.getString(
title = 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),
title = context.getString(R.string.dice_roll_bonus_detail, related),
value = abilityValue.modifier
)
),
) + alterations.map { it.bonus }.flatten(),
)
}
private fun actionRoll(
action: Action?,
throws: Action.Throw?,
): Roll {
// build the title
val title = context.getString(
when (action?.type) {
Action.Type.SPELL -> when (throws === action.hit) {
true -> R.string.dice_roll_spell_hit_title
else -> R.string.dice_roll_spell_damage_title
}
else -> when (throws === action?.hit) {
true -> R.string.dice_roll_attack_hit_title
else -> R.string.dice_roll_attack_damage_title
}
},
action?.title,
)
// get the alteration for roll and a given player
val alterations = if (action?.type == Action.Type.ATTACK) {
alterationRepository.getStatus(
character = model.name,
property = when (throws === action.hit) {
true -> Property.ATTACK_ROLL
else -> Property.ATTACK_DAMAGE_ROLL
},
)
} else {
emptyList()
}
// check if any alteration give us advantage or disadvantage
val advantage = alterations.any { it.advantage }
val disadvantage = alterations.any { it.disadvantage }
val fail = alterations.any { it.fail }
// build the roll
return Roll(
title = title,
highlight = action?.title,
dices = listOf(
Roll.Dice(
title = action?.title,
advantage = advantage,
disadvantage = disadvantage,
fail = fail,
count = throws?.amount ?: 1,
faces = throws?.faces ?: 20,
)
) + alterations.map { it.dices }.flatten(),
bonus = (throws?.modifier
?.mapNotNull {
when (it) {
Property.PROFICIENCY -> {
Roll.Bonus(
title = context.getString(R.string.dice_roll_proficiency_bonus),
value = model.proficiency,
)
}
Property.STRENGTH -> {
val related = context.getString(R.string.character_sheet_stat_strength)
Roll.Bonus(
title = context.getString(R.string.dice_roll_bonus_detail, related),
value = model.strength.modifier,
)
}
Property.DEXTERITY -> {
val related = context.getString(R.string.character_sheet_stat_dexterity)
Roll.Bonus(
title = context.getString(R.string.dice_roll_bonus_detail, related),
value = model.dexterity.modifier,
)
}
Property.CONSTITUTION -> {
val related =
context.getString(R.string.character_sheet_stat_constitution)
Roll.Bonus(
title = context.getString(R.string.dice_roll_bonus_detail, related),
value = model.constitution.modifier,
)
}
Property.INTELLIGENCE -> {
val related =
context.getString(R.string.character_sheet_stat_intelligence)
Roll.Bonus(
title = context.getString(R.string.dice_roll_bonus_detail, related),
value = model.intelligence.modifier,
)
}
Property.WISDOM -> {
val related = context.getString(R.string.character_sheet_stat_wisdom)
Roll.Bonus(
title = context.getString(R.string.dice_roll_bonus_detail, related),
value = model.wisdom.modifier,
)
}
Property.CHARISMA -> {
val related = context.getString(R.string.character_sheet_stat_charisma)
Roll.Bonus(
title = context.getString(R.string.dice_roll_bonus_detail, related),
value = model.charisma.modifier,
)
}
else -> null
}
} ?: emptyList()) + alterations.map { it.bonus }.flatten(),
)
}
// endregion
companion object {
const val TAG = "CharacterSheetViewModel"
}
}

View file

@ -0,0 +1,126 @@
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.DrawableRes
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
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.shape.CutCornerShape
import androidx.compose.material3.Button
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.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.res.painterResource
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
@Stable
data class ActionsUio(
val title: String,
@DrawableRes val hit: Int?,
@DrawableRes val damage: Int?,
)
@Composable
fun Action(
modifier: Modifier = Modifier,
padding: PaddingValues = PaddingValues(start = 16.dp, end = 8.dp, top = 4.dp, bottom = 4.dp),
action: ActionsUio,
onHit: (id: String) -> Unit,
onDamage: (id: String) -> Unit,
) {
Row(
modifier = modifier
.ddBorder(
outline = remember { CutCornerShape(size = 16.dp) },
inner = RectangleShape,
)
.padding(paddingValues = padding),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(
modifier = Modifier.weight(1f),
style = MaterialTheme.typography.bodyMedium,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
text = action.title,
)
action.hit?.let {
Button(
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp),
onClick = { onHit(action.title) },
) {
Icon(
modifier = Modifier.size(size = 24.dp),
painter = painterResource(id = it),
contentDescription = null,
)
}
}
action.damage?.let {
Button(
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp),
onClick = { onDamage(action.title) },
) {
Icon(
modifier = Modifier.size(size = 24.dp),
painter = painterResource(id = it),
contentDescription = null,
)
}
}
}
}
@Composable
@Preview(uiMode = UI_MODE_NIGHT_NO)
@Preview(uiMode = UI_MODE_NIGHT_YES)
private fun ActionPreview(
@PreviewParameter(ActionPreviewProvider::class) preview: ActionsUio
) {
LexiconTheme {
Surface {
Action(
modifier = Modifier.fillMaxWidth(),
action = preview,
onHit = { },
onDamage = { },
)
}
}
}
private class ActionPreviewProvider : PreviewParameterProvider<ActionsUio> {
override val values: Sequence<ActionsUio> = sequenceOf(
ActionsUio(
title = "Hache d'arme",
hit = R.drawable.ic_d20_24,
damage = R.drawable.ic_d8_24,
),
ActionsUio(
title = "Explosion occulte",
hit = R.drawable.ic_d20_24,
damage = R.drawable.ic_d10_24,
)
)
}

View file

@ -0,0 +1,163 @@
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.compose.foundation.BorderStroke
import androidx.compose.foundation.background
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.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CutCornerShape
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.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.text.font.FontWeight
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.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.extentions.ddBorder
import com.pixelized.rplexicon.utilitary.extentions.lexicon
@Stable
data class CounterUio(
val title: String,
val value: Int,
val max: Int?,
)
@Composable
fun Counter(
modifier: Modifier = Modifier,
padding: PaddingValues = PaddingValues(start = 16.dp, end = 8.dp, top = 4.dp, bottom = 4.dp),
counter: CounterUio,
) {
Box(
modifier = modifier.ddBorder(
outline = remember { CutCornerShape(size = 16.dp) },
inner = RectangleShape,
),
contentAlignment = Alignment.CenterStart,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 40.dp)
.padding(paddingValues = padding),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
modifier = Modifier.weight(1f),
style = MaterialTheme.typography.bodyMedium,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = counter.title,
)
if (counter.max != null && counter.max <= 8) {
repeat(counter.max) { index ->
Marker(
filled = (counter.max - index) > counter.value,
)
}
} else {
Text(
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Bold,
text = "${counter.value}",
)
counter.max?.let { max ->
Text(
style = MaterialTheme.typography.labelSmall,
text = "/",
)
Text(
style = MaterialTheme.typography.bodyMedium,
text = "$max",
)
}
}
}
}
}
@Composable
private fun Marker(
modifier: Modifier = Modifier,
filled: Boolean,
) {
Surface(
modifier = modifier,
tonalElevation = 8.dp,
border = BorderStroke(width = 1.dp, color = MaterialTheme.lexicon.colorScheme.handle),
) {
Box(
modifier = Modifier
.padding(all = 4.dp)
.size(16.dp)
.background(
color = when (filled) {
true -> MaterialTheme.colorScheme.primary
else -> Color.Transparent
}
)
)
}
}
@Composable
@Preview(uiMode = UI_MODE_NIGHT_NO)
@Preview(uiMode = UI_MODE_NIGHT_YES)
private fun CounterPreview(
@PreviewParameter(CounterPreviewProvider::class) preview: CounterUio
) {
LexiconTheme {
Surface {
Counter(counter = preview)
}
}
}
private class CounterPreviewProvider : PreviewParameterProvider<CounterUio> {
override val values: Sequence<CounterUio> = sequenceOf(
CounterUio(
title = "Rage",
value = 2,
max = 2,
),
CounterUio(
title = "Dé de vie",
value = 0,
max = 1,
),
CounterUio(
title = "Sort de niveau 1",
value = 3,
max = 8,
),
CounterUio(
title = "Point de vie",
value = 15,
max = null,
),
CounterUio(
title = "Point de vie",
value = 15,
max = 25,
),
)
}

View file

@ -2,6 +2,7 @@ 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.Box
import androidx.compose.foundation.layout.PaddingValues
@ -36,11 +37,14 @@ data class ProficiencyUio(
@Composable
fun Proficiency(
modifier: Modifier = Modifier,
padding: PaddingValues = PaddingValues(start = 16.dp, end = 27.dp),
padding: PaddingValues = PaddingValues(horizontal = 8.dp),
proficiency: ProficiencyUio,
onClick: () -> Unit,
) {
Box(
modifier = modifier.heightIn(48.dp),
modifier = modifier
.heightIn(48.dp)
.clickable(onClick = onClick),
contentAlignment = Alignment.CenterStart,
) {
Row(
@ -49,7 +53,7 @@ fun Proficiency(
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
Box(
modifier = Modifier.size(size = 32.dp),
modifier = Modifier.size(size = 24.dp),
contentAlignment = Alignment.Center,
) {
MasteryCircle(
@ -91,6 +95,7 @@ private fun ProficiencyPreview(
Surface {
Proficiency(
proficiency = proficiency,
onClick = { },
)
}
}

View file

@ -4,9 +4,11 @@ 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.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CutCornerShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
@ -54,9 +56,14 @@ fun SavingsThrows(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
MasteryCircle(
multiplier = savingsThrows.multiplier,
)
Box(
modifier = Modifier.size(size = 24.dp),
contentAlignment = Alignment.Center,
) {
MasteryCircle(
multiplier = savingsThrows.multiplier,
)
}
Text(
modifier = Modifier.weight(weight = 1f, fill = true),

View file

@ -3,6 +3,7 @@ package com.pixelized.rplexicon.ui.screens.lexicon.detail
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.net.Uri
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@ -53,6 +54,7 @@ import com.pixelized.rplexicon.model.Lexicon
import com.pixelized.rplexicon.ui.composable.AsyncImage
import com.pixelized.rplexicon.ui.composable.BackgroundImage
import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost
import com.pixelized.rplexicon.ui.navigation.screens.navigateToCharacterSheet
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.composable.stringResource
import com.pixelized.rplexicon.utilitary.extentions.annotatedSpan
@ -148,7 +150,9 @@ fun LexiconDetailScreen(
LexiconDetailContent(
modifier = Modifier.fillMaxSize(),
item = viewModel.character,
haveCharacterSheet = viewModel.haveCharacterSheet,
onBack = { screen.popBackStack() },
onCharacterSheet = { screen.navigateToCharacterSheet(name = it) },
)
}
}
@ -159,7 +163,9 @@ private fun LexiconDetailContent(
modifier: Modifier = Modifier,
state: ScrollState = rememberScrollState(),
item: State<LexiconDetailUio>,
haveCharacterSheet: State<Boolean>,
onBack: () -> Unit,
onCharacterSheet: (String) -> Unit,
) {
val colorScheme = MaterialTheme.colorScheme
val typography = MaterialTheme.typography
@ -178,6 +184,16 @@ private fun LexiconDetailContent(
)
}
},
actions = {
AnimatedVisibility(visible = haveCharacterSheet.value) {
IconButton(onClick = { onCharacterSheet(item.value.name) }) {
Icon(
painter = painterResource(id = R.drawable.ic_d20_24),
contentDescription = null
)
}
}
},
title = {
Text(text = stringResource(id = R.string.detail_title))
},
@ -358,7 +374,9 @@ private fun LexiconDetailPreview() {
LexiconDetailContent(
modifier = Modifier.fillMaxSize(),
item = character,
haveCharacterSheet = remember { mutableStateOf(true) },
onBack = { },
onCharacterSheet = { },
)
}
}

View file

@ -4,6 +4,7 @@ import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import com.pixelized.rplexicon.repository.data.CharacterSheetRepository
import com.pixelized.rplexicon.repository.data.LexiconRepository
import com.pixelized.rplexicon.ui.navigation.screens.lexiconDetailArgument
import dagger.hilt.android.lifecycle.HiltViewModel
@ -12,13 +13,15 @@ import javax.inject.Inject
@HiltViewModel
class LexiconDetailViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
repository: LexiconRepository,
lexiconRepository: LexiconRepository,
characterSheetRepository: CharacterSheetRepository,
) : ViewModel() {
val character: State<LexiconDetailUio>
val haveCharacterSheet: State<Boolean>
init {
val argument = savedStateHandle.lexiconDetailArgument
val source = repository.data.value[argument.id]
val source = lexiconRepository.data.value[argument.id]
character = mutableStateOf(
LexiconDetailUio(
@ -35,5 +38,9 @@ class LexiconDetailViewModel @Inject constructor(
highlightRace = argument.highlightRace && argument.race == source.race,
)
)
haveCharacterSheet = mutableStateOf(
characterSheetRepository.find(source.name) != null
)
}
}

View file

@ -4,7 +4,6 @@ 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.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.clickable
@ -63,7 +62,7 @@ fun LexiconScreen(
refreshing = false,
onRefresh = {
scope.launch {
viewModel.fetchLexicon()
viewModel.updateLexicon()
}
},
)
@ -92,16 +91,13 @@ fun LexiconScreen(
HandleFetchError(
errors = viewModel.error,
onPermissionGranted = {
viewModel.fetchLexicon()
viewModel.updateLexicon()
}
)
}
}
@OptIn(
ExperimentalMaterialApi::class,
ExperimentalAnimationApi::class,
)
@OptIn(ExperimentalMaterialApi::class)
@Composable
private fun LexiconScreenContent(
modifier: Modifier = Modifier,

View file

@ -8,10 +8,15 @@ import androidx.lifecycle.viewModelScope
import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.model.Lexicon
import com.pixelized.rplexicon.repository.data.ActionRepository
import com.pixelized.rplexicon.repository.data.AlterationRepository
import com.pixelized.rplexicon.repository.data.CharacterSheetRepository
import com.pixelized.rplexicon.repository.data.LexiconRepository
import com.pixelized.rplexicon.ui.composable.error.FetchErrorUio
import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.launch
@ -19,7 +24,10 @@ import javax.inject.Inject
@HiltViewModel
class LexiconViewModel @Inject constructor(
private val repository: LexiconRepository,
private val lexiconRepository: LexiconRepository,
private val characterSheetRepository: CharacterSheetRepository,
private val alterationRepository: AlterationRepository,
private val actionRepository: ActionRepository,
) : ViewModel() {
private val _isLoading = mutableStateOf(false)
@ -33,7 +41,7 @@ class LexiconViewModel @Inject constructor(
init {
viewModelScope.launch {
repository.data.collect { items ->
lexiconRepository.data.collect { items ->
_items.value = items.map { item ->
LexiconItemUio(
id = item.id,
@ -66,14 +74,51 @@ class LexiconViewModel @Inject constructor(
}
viewModelScope.launch {
fetchLexicon()
_isLoading.value = true
val characterRequest = async {
try {
characterSheetRepository.fetchCharacterSheet()
actionRepository.fetchActions()
} catch (exception: Exception) {
Log.e(TAG, exception.message, exception)
}
}
val alterationRequest = async {
try {
alterationRepository.fetchAlterationSheet()
} catch (exception: Exception) {
Log.e(TAG, exception.message, exception)
}
}
val statusRequest = async {
try {
alterationRepository.fetchStatusSheet()
} catch (exception: Exception) {
Log.e(TAG, exception.message, exception)
}
}
val lexiconRequest = async {
fetchLexicon()
}
awaitAll(
characterRequest,
alterationRequest,
statusRequest,
lexiconRequest,
)
_isLoading.value = false
}
}
suspend fun fetchLexicon() {
suspend fun updateLexicon() {
_isLoading.value = true
fetchLexicon()
_isLoading.value = false
}
private suspend fun fetchLexicon() {
try {
_isLoading.value = true
repository.fetchLexicon()
lexiconRepository.fetchLexicon()
}
// user need to accept OAuth2 permission.
catch (exception: UserRecoverableAuthIOException) {
@ -88,10 +133,6 @@ class LexiconViewModel @Inject constructor(
Log.e(TAG, exception.message, exception)
_error.emit(FetchErrorUio.Default)
}
// clean the laoding state
finally {
_isLoading.value = false
}
}
companion object {

View file

@ -4,7 +4,7 @@ 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.facotry.displayable.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

View file

@ -28,16 +28,18 @@ class LexiconTypography(
val stamp: TextStyle = base.headlineLarge.copy(
fontFamily = stampFontFamily,
),
val bodyDropCapSpan: SpanStyle = base.displaySmall.copy(
val bodyDropCap: TextStyle = base.displaySmall.copy(
fontFamily = regalFontFamily,
baselineShift = BaselineShift(-0.3f),
letterSpacing = (-6).sp
).toSpanStyle(),
val titleDropCapSpan: SpanStyle = base.displayLarge.copy(
),
val titleDropCap: TextStyle = base.displayLarge.copy(
fontFamily = regalFontFamily,
baselineShift = BaselineShift(-0.3f),
letterSpacing = (-8).sp
).toSpanStyle()
),
val bodyDropCapSpan: SpanStyle = bodyDropCap.toSpanStyle(),
val titleDropCapSpan: SpanStyle = titleDropCap.toSpanStyle(),
)
fun lexiconTypography() = LexiconTypography()

View file

@ -18,6 +18,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
@ -82,9 +83,10 @@ fun Modifier.ddBorder(
innerWidth: Dp = 1.dp,
): Modifier = composed {
val isDarkTheme = isSystemInDarkTheme()
val elevation = remember { derivedStateOf { if (isDarkTheme) 2.dp else 0.dp } }
val colorScheme = MaterialTheme.lexicon.colorScheme
return@composed this
return@composed this then Modifier
.border(
width = outlineWidth,
color = colorScheme.characterSheet.outlineBorder,
@ -99,23 +101,11 @@ fun Modifier.ddBorder(
color = colorScheme.characterSheet.innerBorder,
shape = inner,
)
.background(
shape = inner,
color = colorScheme.base.surfaceColorAtElevation(elevation.value)
)
.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
}

View file

@ -57,6 +57,10 @@
<string name="map_label">Coordonnées</string>
<string name="character_sheet_title">Feuille de personnage</string>
<string name="character_sheet_title_saving_throws">Jet de sauvegarde</string>
<string name="character_sheet_title_proficiencies">Maîtrises</string>
<string name="character_sheet_title_actions">Actions</string>
<string name="character_sheet_title_alteration">Altérations</string>
<string name="character_sheet_stat_strength">Force</string>
<string name="character_sheet_stat_strength_short">FOR</string>
<string name="character_sheet_stat_dexterity">Dextérité</string>
@ -69,7 +73,6 @@
<string name="character_sheet_stat_wisdom_short">SAG</string>
<string name="character_sheet_stat_charisma">Charisme</string>
<string name="character_sheet_stat_charisma_short">CHA</string>
<string name="character_sheet_saving_throws">Jet de sauvegarde</string>
<string name="character_sheet_proficiency">Talent</string>
<string name="character_sheet_proficiency_acrobatics">Acrobaties</string>
<string name="character_sheet_proficiency_animal_handling">Dressage</string>
@ -94,9 +97,15 @@
<string name="dice_roll_mastery_expertise">Expertise \"%1$s\" </string>
<string name="dice_roll_mastery_saving_throw">Jet de sauvegarde</string>
<string name="dice_roll_proficiency_bonus">Bonus de Maîtrise</string>
<string name="dice_roll_expertise_bonus">Bonus d\'Expertise</string>
<string name="dice_roll_check_title">TEST \"%1$s\"</string>
<string name="dice_roll_check_detail">Test \"%1$s\"</string>
<string name="dice_roll_bonus_detail">Bonus \"%1$s\"</string>
<string name="dice_roll_attack_hit_title">Jet d\'attaque : \"%1$s\"</string>
<string name="dice_roll_attack_damage_title">Jet de dommage : \"%1$s\"</string>
<string name="dice_roll_spell_hit_title">Jet de sort : \"%1$s"</string>
<string name="dice_roll_spell_damage_title">Jet de dommage : \"%1$s\"</string>
<string name="dice_roll_saving_throw_title">JET DE SAUVEGARDE : %1$s</string>
<string name="dice_roll_saving_throw_detail">Sauvegarde de \"%1$s\"</string>

View file

@ -57,6 +57,10 @@
<string name="map_label">Coordinates</string>
<string name="character_sheet_title">Character sheet</string>
<string name="character_sheet_title_saving_throws">Saving Throws</string>
<string name="character_sheet_title_proficiencies">Proficiencies</string>
<string name="character_sheet_title_actions">Actions</string>
<string name="character_sheet_title_alteration">Alterations</string>
<string name="character_sheet_stat_strength">Strength</string>
<string name="character_sheet_stat_strength_short">STR</string>
<string name="character_sheet_stat_dexterity">Dexterity</string>
@ -69,7 +73,6 @@
<string name="character_sheet_stat_wisdom_short">WIS</string>
<string name="character_sheet_stat_charisma">Charisma</string>
<string name="character_sheet_stat_charisma_short">CHA</string>
<string name="character_sheet_saving_throws">Saving Throws</string>
<string name="character_sheet_proficiency">Proficiency</string>
<string name="character_sheet_proficiency_acrobatics">Acrobatics</string>
<string name="character_sheet_proficiency_animal_handling">Animal Handling</string>
@ -94,9 +97,15 @@
<string name="dice_roll_mastery_expertise">%1$s expertise</string>
<string name="dice_roll_mastery_saving_throw">Saving throw</string>
<string name="dice_roll_proficiency_bonus">Proficiency bonus</string>
<string name="dice_roll_expertise_bonus">Expertise bonus</string>
<string name="dice_roll_check_title">%1$s CHECK</string>
<string name="dice_roll_check_detail">%1$s check</string>
<string name="dice_roll_bonus_detail">%1$s bonus</string>
<string name="dice_roll_attack_hit_title">%1$s HIT</string>
<string name="dice_roll_attack_damage_title">%1$s DAMAGE</string>
<string name="dice_roll_spell_hit_title">%1$s HIT</string>
<string name="dice_roll_spell_damage_title">%1$s DAMAGE</string>
<string name="dice_roll_saving_throw_title">%1$s SAVING THROW</string>
<string name="dice_roll_saving_throw_detail">%1$s save</string>