Add proper skills management.

This commit is contained in:
Thomas Andres Gomez 2023-10-09 16:07:33 +02:00
parent 6528b89f6b
commit 7b6f5b6430
45 changed files with 1355 additions and 706 deletions

View file

@ -1,11 +1,11 @@
package com.pixelized.rplexicon
import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.pixelized.rplexicon.model.Description
import com.pixelized.rplexicon.repository.data.ActionRepository
import com.pixelized.rplexicon.repository.data.AlterationRepository
import com.pixelized.rplexicon.repository.data.CharacterSheetRepository
@ -13,6 +13,7 @@ import com.pixelized.rplexicon.repository.data.DescriptionRepository
import com.pixelized.rplexicon.repository.data.LexiconRepository
import com.pixelized.rplexicon.repository.data.LocationRepository
import com.pixelized.rplexicon.repository.data.QuestRepository
import com.pixelized.rplexicon.repository.data.SkillRepository
import com.pixelized.rplexicon.repository.data.SpellRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.async
@ -31,6 +32,7 @@ class LauncherViewModel @Inject constructor(
characterSheetRepository: CharacterSheetRepository,
actionRepository: ActionRepository,
spellRepository: SpellRepository,
skillRepository: SkillRepository,
descriptionRepository: DescriptionRepository,
) : ViewModel() {
@ -46,6 +48,7 @@ class LauncherViewModel @Inject constructor(
try {
lexiconRepository.fetchLexicon()
} catch (exception: Exception) {
Log.e(TAG, exception.message, exception)
_error.tryEmit("Lexicon fail to update")
}
}
@ -53,6 +56,7 @@ class LauncherViewModel @Inject constructor(
try {
locationRepository.fetchLocation()
} catch (exception: Exception) {
Log.e(TAG, exception.message, exception)
_error.tryEmit("Location fail to update")
}
}
@ -60,6 +64,7 @@ class LauncherViewModel @Inject constructor(
try {
questRepository.fetchQuests()
} catch (exception: Exception) {
Log.e(TAG, exception.message, exception)
_error.tryEmit("Quest fail to update")
}
}
@ -67,6 +72,7 @@ class LauncherViewModel @Inject constructor(
try {
characterSheetRepository.fetchCharacterSheet()
} catch (exception: Exception) {
Log.e(TAG, exception.message, exception)
_error.tryEmit("CharacterSheet fail to update")
}
}
@ -74,6 +80,7 @@ class LauncherViewModel @Inject constructor(
try {
descriptionRepository.fetchDescription()
} catch (exception: Exception) {
Log.e(TAG, exception.message, exception)
_error.tryEmit("Skill/Spell description fail to update")
}
}
@ -83,6 +90,7 @@ class LauncherViewModel @Inject constructor(
try {
alterationRepository.fetchAlterationSheet(sheets = characterSheetRepository.sheets)
} catch (exception: Exception) {
Log.e(TAG, exception.message, exception)
_error.tryEmit("Alteration lexicon fail to update")
}
}
@ -90,6 +98,7 @@ class LauncherViewModel @Inject constructor(
try {
actionRepository.fetchActions()
} catch (exception: Exception) {
Log.e(TAG, exception.message, exception)
_error.tryEmit("Action fail to update")
}
}
@ -97,12 +106,25 @@ class LauncherViewModel @Inject constructor(
try {
spellRepository.fetchSpells()
} catch (exception: Exception) {
Log.e(TAG, exception.message, exception)
_error.tryEmit("Spell fail to update")
}
}
awaitAll(alteration, action, spell)
val skill = async {
try {
skillRepository.fetchSkills()
} catch (exception: Exception) {
Log.e(TAG, exception.message, exception)
_error.tryEmit("Skill fail to update")
}
}
awaitAll(alteration, action, spell, skill)
isLoading = false
}
}
companion object {
private const val TAG = "LauncherViewModel"
}
}

View file

@ -8,10 +8,12 @@ import com.pixelized.rplexicon.model.AssignedSpell
import com.pixelized.rplexicon.model.CharacterSheet
import com.pixelized.rplexicon.model.DiceThrow
import com.pixelized.rplexicon.model.Property
import com.pixelized.rplexicon.model.Skill
import com.pixelized.rplexicon.model.Throw
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.SkillRepository
import com.pixelized.rplexicon.repository.data.SpellRepository
import com.pixelized.rplexicon.ui.screens.rolls.composable.RollDiceUio
import com.pixelized.rplexicon.ui.screens.rolls.composable.ThrowsCardUio
@ -41,6 +43,7 @@ class DiceThrowUseCase @Inject constructor(
private val characterSheetRepository: CharacterSheetRepository,
private val actionRepository: ActionRepository,
private val spellRepository: SpellRepository,
private val skillRepository: SkillRepository,
private val alterationRepository: AlterationRepository,
) {
fun roll(diceThrow: DiceThrow, alterationId: List<String>): DiceThrowResult? {
@ -432,6 +435,16 @@ class DiceThrowUseCase @Inject constructor(
alterations = alterations,
)
}
is DiceThrow.Skill -> {
val skill = skillRepository.find(
character = diceThrow.character,
skill = diceThrow.skill,
)
skillThrow(
skill = skill,
)
}
}
} else {
null
@ -894,6 +907,50 @@ class DiceThrowUseCase @Inject constructor(
)
}
private fun skillThrow(
skill: Skill?,
): DiceThrowResult {
// retrieve some wording.
val spellName = skill?.name
val titleString = application.getString(R.string.dice_roll_spell_cast, spellName)
// create a list destined to contain all the values (rolled + bonus)
val allValue = mutableListOf<Int>()
// main roll
val result = roll(
amount = skill?.effect?.amount ?: 1,
faces = skill?.effect?.faces ?: 4,
)
allValue.add(result.value)
// build the result.
return DiceThrowResult(
dice = RollDiceUio(
icon = (skill?.effect?.faces ?: 4).icon,
result = "${result.value}",
),
card = ThrowsCardUio(
title = titleString.uppercase(),
highlight = spellName,
dice = (skill?.effect?.faces ?: 4).icon,
roll = allValue.toLabel(),
result = "${allValue.sum()}",
details = listOf(
ThrowsCardUio.Detail(
title = spellName,
throws = ThrowsCardUio.Throw(
dice = (skill?.effect?.faces ?: 4).icon,
roll = "${skill?.effect?.amount ?: 1}d${skill?.effect?.faces ?: 4}",
result = result.label,
),
result = "${result.value}",
),
),
)
)
}
private fun roll(
amount: Int = 1,
faces: Int = 20,

View file

@ -11,57 +11,13 @@ data class CharacterSheetFire(
@set:PropertyName(HIT_POINT)
var hitPoint: HitPoint? = null,
@get:PropertyName(RAGE)
@set:PropertyName(RAGE)
var rage: Int? = null,
@get:PropertyName(SKILLS)
@set:PropertyName(SKILLS)
var skills: Map<String, Int> = emptyMap(),
@get:PropertyName(RELENTLESS_ENDURANCE)
@set:PropertyName(RELENTLESS_ENDURANCE)
var relentlessEndurance: Int? = null,
@get:PropertyName(DIVINE_CONDUIT)
@set:PropertyName(DIVINE_CONDUIT)
var divineConduit: Int? = null,
@get:PropertyName(BARDIC_INSPIRATION)
@set:PropertyName(BARDIC_INSPIRATION)
var bardicInspiration: Int? = null,
@get:PropertyName("${SPELL_SLOT_LEVEL_X}1")
@set:PropertyName("${SPELL_SLOT_LEVEL_X}1")
var spell1: Int? = null,
@get:PropertyName("${SPELL_SLOT_LEVEL_X}2")
@set:PropertyName("${SPELL_SLOT_LEVEL_X}2")
var spell2: Int? = null,
@get:PropertyName("${SPELL_SLOT_LEVEL_X}3")
@set:PropertyName("${SPELL_SLOT_LEVEL_X}3")
var spell3: Int? = null,
@get:PropertyName("${SPELL_SLOT_LEVEL_X}4")
@set:PropertyName("${SPELL_SLOT_LEVEL_X}4")
var spell4: Int? = null,
@get:PropertyName("${SPELL_SLOT_LEVEL_X}5")
@set:PropertyName("${SPELL_SLOT_LEVEL_X}5")
var spell5: Int? = null,
@get:PropertyName("${SPELL_SLOT_LEVEL_X}6")
@set:PropertyName("${SPELL_SLOT_LEVEL_X}6")
var spell6: Int? = null,
@get:PropertyName("${SPELL_SLOT_LEVEL_X}7")
@set:PropertyName("${SPELL_SLOT_LEVEL_X}7")
var spell7: Int? = null,
@get:PropertyName("${SPELL_SLOT_LEVEL_X}8")
@set:PropertyName("${SPELL_SLOT_LEVEL_X}8")
var spell8: Int? = null,
@get:PropertyName("${SPELL_SLOT_LEVEL_X}9")
@set:PropertyName("${SPELL_SLOT_LEVEL_X}9")
var spell9: Int? = null,
@get:PropertyName(SPELLS)
@set:PropertyName(SPELLS)
var spells: Map<String, Int> = emptyMap(),
) {
@Keep
@IgnoreExtraProperties
@ -77,10 +33,8 @@ data class CharacterSheetFire(
companion object {
const val HIT_POINT = "hit_point"
const val RAGE = "rage"
const val RELENTLESS_ENDURANCE = "relentless_endurance"
const val DIVINE_CONDUIT = "divine_conduit"
const val BARDIC_INSPIRATION = "bardic_inspiration"
const val SPELL_SLOT_LEVEL_X = "spell_slot_level_"
const val SKILLS = "skills"
const val SPELLS = "spells"
const val SPELL_PREFIX = "lvl_"
}
}

View file

@ -39,4 +39,5 @@ sealed class DiceThrow(val character: String) {
class SpellAttack(character: String, val spell: String) : DiceThrow(character)
class SpellDamage(character: String, val spell: String) : DiceThrow(character)
class SpellEffect(character: String, val spell: String, val level: Int) : DiceThrow(character)
class Skill(character: String, val skill: String) : DiceThrow(character)
}

View file

@ -0,0 +1,9 @@
package com.pixelized.rplexicon.model
data class Skill(
val name: String,
val amount: Int?,
val rest: String?,
val cost: String?,
val effect: Throw?,
)

View file

@ -75,16 +75,22 @@ class FirebaseRepository @Inject constructor(
)
}
fun setToken(character: String, token: String, value: Int) {
fun setSkill(character: String, name: String, value: Int) {
val reference = database.getReference(
"$PATH_CHARACTERS/$character/$token"
"$PATH_CHARACTERS/$character/${CharacterSheetFire.SKILLS}/$name"
)
reference.setValue(value)
}
fun setSpell(character: String, level: Int, value: Int) {
val reference = database.getReference(
"$PATH_CHARACTERS/$character/${CharacterSheetFire.SPELLS}/${CharacterSheetFire.SPELL_PREFIX}$level"
)
reference.setValue(value)
}
companion object {
private const val TAG = "FirebaseRepository"
private const val PATH_CHARACTERS = "Characters"
}
}

View file

@ -1,6 +1,5 @@
package com.pixelized.rplexicon.repository.data
import com.pixelized.rplexicon.model.Description
object Sheet {
object Lexicon {
@ -18,6 +17,7 @@ object Sheet {
const val CHARACTER = "Feuille de personnage"
const val ATTACK = "Attaques"
const val MAGIC = "Magies"
const val SKILL = "Capacités"
const val MAGIC_LEXICON = "Lexique magique"
const val ALTERATION = "Altérations"
const val DESCRIPTION = "Descriptions"

View file

@ -0,0 +1,38 @@
package com.pixelized.rplexicon.repository.data
import com.pixelized.rplexicon.model.Skill
import com.pixelized.rplexicon.repository.GoogleSheetServiceRepository
import com.pixelized.rplexicon.repository.parser.SkillParser
import com.pixelized.rplexicon.utilitary.Update
import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class SkillRepository @Inject constructor(
private val googleRepository: GoogleSheetServiceRepository,
private val skillParser: SkillParser,
) {
private val _skills = MutableStateFlow<Map<String, List<Skill>>>(emptyMap())
val skills: StateFlow<Map<String, List<Skill>>> get() = _skills
var lastSuccessFullUpdate: Update = Update.INITIAL
private set
fun find(character: String, skill: String): Skill? {
return skills.value[character]?.firstOrNull { it.name == skill }
}
@Throws(IncompatibleSheetStructure::class, Exception::class)
suspend fun fetchSkills() {
googleRepository.fetch { sheet ->
val request = sheet.get(Sheet.Character.ID, Sheet.Character.SKILL)
val skills = skillParser.parse(data = request.execute())
_skills.emit(skills)
lastSuccessFullUpdate = Update.currentTime()
}
}
}

View file

@ -0,0 +1,64 @@
package com.pixelized.rplexicon.repository.parser
import com.google.api.services.sheets.v4.model.ValueRange
import com.pixelized.rplexicon.model.Skill
import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure
import com.pixelized.rplexicon.utilitary.extentions.local.checkSheetStructure
import com.pixelized.rplexicon.utilitary.extentions.sheet
import javax.inject.Inject
class SkillParser @Inject constructor(
private val throwParser: ThrowParser,
) {
@Throws(IncompatibleSheetStructure::class)
fun parse(data: ValueRange): Map<String, List<Skill>> {
val sheet = data.values.sheet()
val values = hashMapOf<String, MutableList<Skill>>()
lateinit var sheetStructure: Map<String, Int>
fun List<*>.parse(column: String): String? =
(getOrNull(sheetStructure.getValue(column)) as? String)?.takeIf { it.isNotBlank() }
sheet?.forEachIndexed { index, row ->
when {
index == 0 -> {
sheetStructure = row.checkSheetStructure(model = COLUMNS)
}
row is List<*> -> {
val character = row[0] as? String
val name = row.parse(column = NAME)
if (character?.isNotBlank() == true && name != null) {
val skill = Skill(
name = name,
amount = row.parse(column = AMOUNT)?.toIntOrNull(),
rest = row.parse(column = REST),
cost = row.parse(column = COST),
effect = throwParser.parse(row.parse(column = EFFECT)),
)
values.getOrPut(character) { mutableListOf() }.add(skill)
}
}
}
}
return values
}
companion object {
private const val NAME = "Nom"
private const val AMOUNT = "Quantité"
private const val REST = "Récupération"
private const val COST = "Coût"
private const val EFFECT = "Effect"
private val COLUMNS = listOf(
NAME,
AMOUNT,
REST,
COST,
EFFECT,
)
}
}

View file

@ -4,6 +4,7 @@ import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.height
@ -12,10 +13,12 @@ import androidx.compose.foundation.pager.PageSize
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.VerticalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -48,7 +51,11 @@ fun NumberPicker(
) { index ->
Box(
modifier = Modifier
.clickable { scope.launch { pager.animateScrollToPage(page = index) } }
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false),
onClick = { scope.launch { pager.animateScrollToPage(page = index) } },
)
.size(size = itemSize),
contentAlignment = Alignment.Center,
) {

View file

@ -18,7 +18,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
@ -30,8 +29,7 @@ import com.pixelized.rplexicon.utilitary.extentions.ddBorder
import com.pixelized.rplexicon.utilitary.extentions.lexicon
@Stable
data class CounterEditDialogUio(
val id: String,
data class SkillEditDialogUio(
val label: String,
val value: Int,
val max: Int,
@ -39,13 +37,12 @@ data class CounterEditDialogUio(
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun HandleCounterEditDialog(
dialog: State<CounterEditDialogUio?>,
fun HandleSkillEditDialog(
dialog: State<SkillEditDialogUio?>,
onDismissRequest: () -> Unit,
onConfirm: (String, Int) -> Unit,
onConfirm: (name: String, value: Int) -> Unit,
) {
dialog.value?.let {
val scope = rememberCoroutineScope()
val pager = rememberPagerState(initialPage = it.value) { it.max + 1 }
Dialog(
properties = remember { DialogProperties(usePlatformDefaultWidth = false) },
@ -75,7 +72,7 @@ fun HandleCounterEditDialog(
pager = pager,
)
TextButton(
onClick = { onConfirm(it.id, pager.currentPage) },
onClick = { onConfirm(it.label, pager.currentPage) },
) {
Text(text = stringResource(id = android.R.string.ok))
}

View file

@ -0,0 +1,84 @@
package com.pixelized.rplexicon.ui.composable.edit
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CutCornerShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.ui.composable.NumberPicker
import com.pixelized.rplexicon.utilitary.extentions.ddBorder
import com.pixelized.rplexicon.utilitary.extentions.lexicon
@Stable
data class SpellEditDialogUio(
val level: Int,
val value: Int,
val max: Int,
)
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun HandleSpellEditDialog(
dialog: State<SpellEditDialogUio?>,
onDismissRequest: () -> Unit,
onConfirm: (level: Int, value: Int) -> Unit,
) {
dialog.value?.let {
val pager = rememberPagerState(initialPage = it.value) { it.max + 1 }
Dialog(
properties = remember { DialogProperties(usePlatformDefaultWidth = false) },
onDismissRequest = onDismissRequest,
) {
Surface(
modifier = Modifier.ddBorder(
inner = remember { RoundedCornerShape(size = 8.dp) },
outline = remember { CutCornerShape(size = 16.dp) },
)
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp),
style = MaterialTheme.typography.labelSmall,
text = stringResource(id = R.string.spell_level_chooser_label, it.level),
)
Box(
modifier = Modifier
.size(width = 64.dp, height = 1.dp)
.background(color = MaterialTheme.lexicon.colorScheme.characterSheet.innerBorder),
)
NumberPicker(
modifier = Modifier.width(width = 128.dp),
pager = pager,
)
TextButton(
onClick = { onConfirm(it.level, pager.currentPage) },
) {
Text(text = stringResource(id = android.R.string.ok))
}
}
}
}
}
}

View file

@ -57,8 +57,8 @@ import com.pixelized.rplexicon.ui.screens.character.composable.character.Profici
import com.pixelized.rplexicon.ui.screens.character.composable.character.StatUio.ID.*
import com.pixelized.rplexicon.ui.screens.character.pages.actions.ActionPage
import com.pixelized.rplexicon.ui.screens.character.pages.actions.ActionPagePreview
import com.pixelized.rplexicon.ui.screens.character.pages.actions.AttackActionViewModel
import com.pixelized.rplexicon.ui.screens.character.pages.actions.SpellsActionViewModel
import com.pixelized.rplexicon.ui.screens.character.pages.actions.AttacksViewModel
import com.pixelized.rplexicon.ui.screens.character.pages.actions.SpellsViewModel
import com.pixelized.rplexicon.ui.screens.character.pages.alteration.AlterationPage
import com.pixelized.rplexicon.ui.screens.character.pages.alteration.AlterationPagePreview
import com.pixelized.rplexicon.ui.screens.character.pages.alteration.AlterationViewModel
@ -78,8 +78,8 @@ import kotlinx.coroutines.launch
fun CharacterSheetScreen(
viewModel: CharacterSheetViewModel = hiltViewModel(),
proficiencyViewModel: ProficiencyViewModel = hiltViewModel(),
attackViewModel: AttackActionViewModel = hiltViewModel(),
spellsViewModel: SpellsActionViewModel = hiltViewModel(),
attacksViewModel: AttacksViewModel = hiltViewModel(),
spellsViewModel: SpellsViewModel = hiltViewModel(),
alterationsViewModel: AlterationViewModel = hiltViewModel(),
) {
val snack = LocalSnack.current
@ -96,7 +96,7 @@ fun CharacterSheetScreen(
)
val pagerState = rememberPagerState {
val haveSheet = proficiencyViewModel.sheet.value != null
val haveAction = attackViewModel.attacks.value.isNotEmpty()
val haveAction = attacksViewModel.attacks.value.isNotEmpty()
val haveSpell = spellsViewModel.spells.value.isNotEmpty()
val haveAlteration = alterationsViewModel.alterations.value.isNotEmpty()
haveSheet.toInt() + (haveAction || haveSpell).toInt() + haveAlteration.toInt()
@ -140,7 +140,7 @@ fun CharacterSheetScreen(
actions = {
ActionPage(
sheetState = sheetState,
attackViewModel = attackViewModel,
attacksViewModel = attacksViewModel,
spellsViewModel = spellsViewModel,
)
},

View file

@ -10,6 +10,7 @@ import com.pixelized.rplexicon.repository.authentication.FirebaseRepository
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.SkillRepository
import com.pixelized.rplexicon.repository.data.SpellRepository
import com.pixelized.rplexicon.ui.composable.error.FetchErrorUio
import com.pixelized.rplexicon.ui.navigation.screens.characterSheetArgument
@ -28,6 +29,7 @@ class CharacterSheetViewModel @Inject constructor(
private val alterationRepository: AlterationRepository,
private val actionRepository: ActionRepository,
private val spellRepository: SpellRepository,
private val skillRepository: SkillRepository,
private val firebaseRepository: FirebaseRepository,
savedStateHandle: SavedStateHandle,
) : ViewModel() {
@ -99,7 +101,18 @@ class CharacterSheetViewModel @Inject constructor(
Log.e(TAG, exception.message, exception)
}
}
awaitAll(alterations, actions, spells)
val skill = async {
try {
if (force || skillRepository.lastSuccessFullUpdate.shouldUpdate()) {
skillRepository.fetchSkills()
} else {
Unit
}
} catch (exception: Exception) {
Log.e(TAG, exception.message, exception)
}
}
awaitAll(alterations, actions, spells, skill)
_isLoading.value = false
}

View file

@ -1,4 +1,4 @@
package com.pixelized.rplexicon.ui.screens.rolls.composable
package com.pixelized.rplexicon.ui.screens.character.composable.actions
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
@ -30,17 +30,17 @@ import androidx.compose.ui.unit.dp
import com.pixelized.rplexicon.ui.theme.LexiconTheme
@Stable
data class RollAlterationUio(
data class AlterationItemUio(
val label: String,
val subLabel: String?,
val checked: Boolean,
)
@Composable
fun RollAlteration(
fun AlterationItem(
modifier: Modifier = Modifier,
padding: PaddingValues = PaddingValues(horizontal = 16.dp),
alteration: RollAlterationUio,
alteration: AlterationItemUio,
onInfo: (id: String) -> Unit,
onClick: (id: String) -> Unit,
) {
@ -90,11 +90,11 @@ fun RollAlteration(
@Preview(uiMode = UI_MODE_NIGHT_NO)
@Preview(uiMode = UI_MODE_NIGHT_YES)
private fun RollAlterationPreview(
@PreviewParameter(RollAlterationPreviewProvider::class) preview: RollAlterationUio,
@PreviewParameter(RollAlterationPreviewProvider::class) preview: AlterationItemUio,
) {
LexiconTheme {
Surface {
RollAlteration(
AlterationItem(
modifier = Modifier.fillMaxWidth(),
alteration = preview,
onInfo = { },
@ -104,19 +104,19 @@ private fun RollAlterationPreview(
}
}
private class RollAlterationPreviewProvider : PreviewParameterProvider<RollAlterationUio> {
override val values: Sequence<RollAlterationUio> = sequenceOf(
RollAlterationUio(
private class RollAlterationPreviewProvider : PreviewParameterProvider<AlterationItemUio> {
override val values: Sequence<AlterationItemUio> = sequenceOf(
AlterationItemUio(
label = "Critique",
checked = false,
subLabel = null,
),
RollAlterationUio(
AlterationItemUio(
label = "Rage",
checked = true,
subLabel = "Barbare",
),
RollAlterationUio(
AlterationItemUio(
label = "Bénédiction",
checked = false,
subLabel = "Clerc",

View file

@ -0,0 +1,74 @@
package com.pixelized.rplexicon.ui.screens.character.composable.actions
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Divider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
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.unit.dp
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.extentions.lexicon
@Composable
fun AttackHeader(
modifier: Modifier = Modifier,
padding: PaddingValues = PaddingValues(horizontal = 16.dp),
) {
Column(
modifier = modifier,
) {
Surface(
modifier = Modifier.fillMaxWidth(),
) {
Text(
modifier = Modifier.padding(paddingValues = padding),
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Bold,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = stringResource(id = R.string.character_sheet_attack_title).let {
AnnotatedString(
text = it,
spanStyles = listOf(
AnnotatedString.Range(
item = MaterialTheme.lexicon.typography.bodyDropCapSpan,
start = 0,
end = Integer.min(1, it.length),
)
)
)
},
)
}
Divider(
modifier = Modifier.padding(paddingValues = padding),
color = MaterialTheme.lexicon.colorScheme.placeholder,
)
}
}
@Composable
@Preview(uiMode = UI_MODE_NIGHT_NO)
@Preview(uiMode = UI_MODE_NIGHT_YES)
private fun SkillHeaderPreview() {
LexiconTheme {
Surface {
SkillHeader()
}
}
}

View file

@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.Column
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.material3.Icon
import androidx.compose.material3.MaterialTheme
@ -20,7 +21,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
@ -29,6 +29,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.model.Attack
import com.pixelized.rplexicon.ui.screens.character.composable.common.DiceButton
import com.pixelized.rplexicon.ui.theme.LexiconTheme
@Stable
@ -51,13 +52,14 @@ data class AttackUio(
@Composable
fun Attack(
modifier: Modifier = Modifier,
padding: PaddingValues = PaddingValues(vertical = 8.dp, horizontal = 16.dp),
padding: PaddingValues = PaddingValues(top = 4.dp, bottom = 4.dp, start = 16.dp, end = 8.dp),
weapon: AttackUio,
onHit: (String) -> Unit,
onDamage: (String) -> Unit,
) {
Row(
modifier = Modifier
.heightIn(min = 52.dp)
.padding(paddingValues = padding)
.then(other = modifier),
horizontalArrangement = Arrangement.spacedBy(space = 16.dp),
@ -79,7 +81,7 @@ fun Attack(
)
Text(
style = MaterialTheme.typography.labelSmall,
fontStyle = FontStyle.Italic,
fontWeight = FontWeight.Normal,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = stringResource(id = weapon.type),
@ -87,26 +89,32 @@ fun Attack(
weapon.range?.let { range ->
Text(
style = MaterialTheme.typography.labelSmall,
fontStyle = FontStyle.Italic,
fontWeight = FontWeight.Normal,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = range,
)
}
}
weapon.hit?.let { dice ->
DiceButton(
icon = dice.icon,
text = dice.label,
onClick = { weapon.name.let(onHit) }
)
}
weapon.damage?.let { dice ->
DiceButton(
icon = dice.icon,
text = dice.label,
onClick = { weapon.name.let(onDamage) }
)
if (weapon.hit != null || weapon.damage != null) {
Row(
horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
) {
weapon.hit?.let { dice ->
DiceButton(
icon = dice.icon,
text = dice.label,
onClick = { weapon.name.let(onHit) }
)
}
weapon.damage?.let { dice ->
DiceButton(
icon = dice.icon,
text = dice.label,
onClick = { weapon.name.let(onDamage) }
)
}
}
}
}
}

View file

@ -0,0 +1,77 @@
package com.pixelized.rplexicon.ui.screens.character.composable.actions
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Divider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
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.unit.dp
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.extentions.lexicon
@Composable
fun SkillHeader(
modifier: Modifier = Modifier,
padding: PaddingValues = PaddingValues(horizontal = 16.dp),
) {
Column(
modifier = modifier
) {
Surface(
modifier = Modifier.fillMaxWidth(),
) {
Text(
modifier = Modifier.padding(paddingValues = padding),
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Bold,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = stringResource(id = R.string.character_sheet_skill_title).let {
AnnotatedString(
text = it,
spanStyles = listOf(
AnnotatedString.Range(
item = MaterialTheme.lexicon.typography.bodyDropCapSpan,
start = 0,
end = Integer.min(1, it.length),
)
)
)
},
)
}
Divider(
modifier = Modifier.padding(paddingValues = padding),
color = MaterialTheme.lexicon.colorScheme.placeholder,
)
}
}
@Composable
@Preview(uiMode = UI_MODE_NIGHT_NO)
@Preview(uiMode = UI_MODE_NIGHT_YES)
private fun SkillHeaderPreview() {
LexiconTheme {
Surface {
SkillHeader()
}
}
}

View file

@ -0,0 +1,212 @@
package com.pixelized.rplexicon.ui.screens.character.composable.actions
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.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
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.material.icons.Icons
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontStyle
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.R
import com.pixelized.rplexicon.ui.screens.character.composable.common.CounterButton
import com.pixelized.rplexicon.ui.screens.character.composable.common.DiceButton
import com.pixelized.rplexicon.ui.theme.LexiconTheme
@Stable
data class SkillItemUio(
val label: String,
val translate: String?,
val rest: String?,
val cost: String?,
val effect: Dice?,
val value: Int?,
val max: Int?,
val haveDetail: Boolean,
) {
class Dice(
@DrawableRes val icon: Int,
val label: String,
)
}
@Composable
fun SkillItem(
modifier: Modifier = Modifier,
padding: PaddingValues = PaddingValues(top = 4.dp, bottom = 4.dp, start = 16.dp, end = 8.dp),
skill: SkillItemUio,
onInfo: (SkillItemUio) -> Unit,
onThrow: (SkillItemUio) -> Unit,
onSkill: (SkillItemUio) -> Unit,
) {
Box(
modifier = Modifier
.clickable { onSkill(skill) }
.padding(paddingValues = padding)
.then(other = modifier),
contentAlignment = Alignment.Center,
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
) {
Column(
modifier = Modifier.weight(weight = 1f),
verticalArrangement = Arrangement.Center,
) {
Row(
horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
) {
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Bold,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = skill.label,
)
skill.translate?.let {
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.labelSmall,
fontWeight = FontWeight.Light,
fontStyle = FontStyle.Italic,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = it,
)
}
}
skill.cost?.let {
Text(
style = MaterialTheme.typography.labelSmall,
fontWeight = FontWeight.Normal,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = it,
)
}
skill.rest?.let {
Text(
style = MaterialTheme.typography.labelSmall,
fontWeight = FontWeight.Normal,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = it,
)
}
}
if (skill.haveDetail) {
IconButton(onClick = { onInfo(skill) }) {
Icon(
imageVector = Icons.Outlined.Info,
contentDescription = null,
)
}
}
skill.effect?.let { effect ->
DiceButton(
icon = effect.icon,
text = effect.label,
onClick = { onThrow(skill) },
)
}
skill.max?.let {
CounterButton(
value = skill.value,
max = it,
)
}
}
}
}
@Composable
@Preview(uiMode = UI_MODE_NIGHT_NO)
@Preview(uiMode = UI_MODE_NIGHT_YES)
private fun CounterItemPreview(
@PreviewParameter(CounterItemPreviewProvider::class) preview: SkillItemUio
) {
LexiconTheme {
Surface {
SkillItem(
skill = preview,
onSkill = { },
onInfo = { },
onThrow = { },
)
}
}
}
@Composable
@Stable
fun rememberTokenListStatePreview(): State<List<SkillItemUio>> = remember {
val provider = CounterItemPreviewProvider()
mutableStateOf(provider.values.toList())
}
private class CounterItemPreviewProvider : PreviewParameterProvider<SkillItemUio> {
override val values: Sequence<SkillItemUio> = sequenceOf(
SkillItemUio(
label = "Endurance Implacable",
translate = "Relentless Endurance",
rest = "Récupération repos long",
cost = null,
effect = null,
value = 1,
max = 1,
haveDetail = true,
),
SkillItemUio(
label = "Apparence inspirante",
translate = "Mantle of Inspiration",
rest = null,
cost = "Inspiration bardique",
effect = SkillItemUio.Dice(icon = R.drawable.ic_d6_24, label = "2d6"),
value = null,
max = null,
haveDetail = true,
),
SkillItemUio(
label = "Renvoi des morts-vivants",
translate = "Turn Undead",
rest = "Récupération repos long",
cost = "Conduit divin",
effect = null,
value = null,
max = null,
haveDetail = true,
),
)
}

View file

@ -2,14 +2,13 @@ package com.pixelized.rplexicon.ui.screens.character.composable.actions
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.background
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.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Divider
import androidx.compose.material3.MaterialTheme
@ -21,13 +20,13 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
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.R
import com.pixelized.rplexicon.ui.screens.character.composable.common.CounterButton
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.extentions.lexicon
@ -46,77 +45,62 @@ data class SpellHeaderUio(
@Composable
fun SpellHeader(
modifier: Modifier = Modifier,
padding: PaddingValues = PaddingValues(top = 8.dp, bottom = 4.dp),
padding: PaddingValues = PaddingValues(horizontal = 16.dp),
header: SpellHeaderUio,
onSpell: (level: Int, value: Int, max: Int) -> Unit,
) {
Box(
modifier = modifier
.background(color = MaterialTheme.colorScheme.surface)
Column(
modifier = Modifier
.clickable(enabled = header.count != null) {
header.count?.let { onSpell(header.level, it.value, it.max) }
}
.padding(horizontal = 16.dp)
.heightIn(min = 32.dp),
.then(other = modifier),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(paddingValues = padding)
.align(alignment = Alignment.Center),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
Surface {
Row(
modifier = Modifier
.weight(1f)
.alignByBaseline(),
style = MaterialTheme.typography.titleMedium,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = stringResource(
id = when (header.level) {
0 -> R.string.character_sheet_action_spell_level_0
else -> R.string.character_sheet_action_spell_level_X
},
header.level
).let { label ->
AnnotatedString(
text = label,
spanStyles = listOf(
AnnotatedString.Range(
item = MaterialTheme.lexicon.typography.bodyDropCapSpan,
start = 0,
end = Integer.min(1, label.length),
.fillMaxWidth()
.padding(paddingValues = padding),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
modifier = Modifier
.weight(1f)
.alignByBaseline(),
style = MaterialTheme.typography.titleMedium,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = stringResource(
id = when (header.level) {
0 -> R.string.character_sheet_action_spell_level_0
else -> R.string.character_sheet_action_spell_level_X
},
header.level
).let { label ->
AnnotatedString(
text = label,
spanStyles = listOf(
AnnotatedString.Range(
item = MaterialTheme.lexicon.typography.bodyDropCapSpan,
start = 0,
end = Integer.min(1, label.length),
)
)
)
},
)
header.count?.let { count ->
CounterButton(
modifier = Modifier.alignByBaseline().offset(x = 4.dp),
value = count.value,
max = count.max,
)
},
)
header.count?.let { count ->
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.primary,
text = "${count.value}",
)
Text(
modifier = Modifier.alignByBaseline(),
fontWeight = FontWeight.Light,
style = MaterialTheme.typography.labelSmall,
text = "/",
)
Text(
modifier = Modifier.alignByBaseline(),
fontWeight = FontWeight.Light,
style = MaterialTheme.typography.bodySmall,
text = "${count.max}",
)
}
}
}
Divider(
modifier = Modifier.align(alignment = Alignment.BottomCenter),
modifier = Modifier.padding(paddingValues = padding),
color = MaterialTheme.lexicon.colorScheme.placeholder,
)
}

View file

@ -33,6 +33,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.model.Spell
import com.pixelized.rplexicon.ui.screens.character.composable.common.DiceButton
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.extentions.local.icon
@ -59,7 +60,7 @@ data class SpellUio(
@Composable
fun Spell(
modifier: Modifier = Modifier,
padding: PaddingValues = PaddingValues(vertical = 8.dp, horizontal = 16.dp),
padding: PaddingValues = PaddingValues(top = 4.dp, bottom = 4.dp, start = 16.dp, end = 8.dp),
spell: SpellUio,
onClick: (String) -> Unit,
onHit: (String) -> Unit,
@ -106,27 +107,25 @@ fun Spell(
}
Row(
horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
) {
Text(
style = MaterialTheme.typography.labelMedium,
fontStyle = FontStyle.Italic,
style = MaterialTheme.typography.labelSmall,
fontWeight = FontWeight.Normal,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = spell.castingTime,
)
Text(
style = MaterialTheme.typography.labelMedium,
fontStyle = FontStyle.Italic,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = MaterialTheme.typography.labelSmall,
fontWeight = FontWeight.Normal,
text = "-",
)
Text(
style = MaterialTheme.typography.labelMedium,
fontStyle = FontStyle.Italic,
style = MaterialTheme.typography.labelSmall,
fontWeight = FontWeight.Normal,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = spell.duration,
@ -134,27 +133,25 @@ fun Spell(
}
Row(
horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
) {
if (spell.ritual) {
Text(
style = MaterialTheme.typography.labelMedium,
fontStyle = FontStyle.Italic,
style = MaterialTheme.typography.labelSmall,
fontWeight = FontWeight.Medium,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = stringResource(id = R.string.spell_detail_ritual),
)
Text(
style = MaterialTheme.typography.labelMedium,
fontStyle = FontStyle.Italic,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = MaterialTheme.typography.labelSmall,
fontWeight = FontWeight.Normal,
text = "-",
)
}
Text(
style = MaterialTheme.typography.labelMedium,
fontStyle = FontStyle.Italic,
style = MaterialTheme.typography.labelSmall,
fontWeight = FontWeight.Normal,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = spell.range,
@ -162,33 +159,39 @@ fun Spell(
}
}
spell.hit?.let { dice ->
DiceButton(
modifier = Modifier.padding(end = 4.dp),
icon = dice.icon,
text = dice.label,
onClick = { spell.name.let(onHit) }
)
}
spell.effect?.let { dice ->
if (spell.changeWithLevel) {
OutlinedButton(
border = BorderStroke(
width = 1.dp,
color = MaterialTheme.colorScheme.primary,
),
onClick = { onCast(spell.name) },
) {
Text(
text = stringResource(id = R.string.character_sheet_action_spell_cast),
if (spell.hit != null || spell.effect != null) {
Row(
horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
) {
spell.hit?.let { dice ->
DiceButton(
icon = dice.icon,
text = dice.label,
onClick = { spell.name.let(onHit) }
)
}
} else {
DiceButton(
icon = dice.icon,
text = dice.label,
onClick = { spell.name.let(onEffect) }
)
spell.effect?.let { dice ->
if (spell.changeWithLevel) {
OutlinedButton(
modifier = Modifier.padding(end = 8.dp),
border = BorderStroke(
width = 1.dp,
color = MaterialTheme.colorScheme.primary,
),
onClick = { onCast(spell.name) },
) {
Text(
text = stringResource(id = R.string.character_sheet_action_spell_cast),
)
}
} else {
DiceButton(
icon = dice.icon,
text = dice.label,
onClick = { spell.name.let(onEffect) }
)
}
}
}
}
}
@ -203,7 +206,6 @@ private fun SpellPreview(
LexiconTheme {
Surface {
Spell(
modifier = Modifier.padding(horizontal = 16.dp),
spell = preview,
onClick = { },
onHit = { },

View file

@ -63,11 +63,6 @@ fun CharacterSheetHeader(
LabelPoint(label = it)
}
}
Divider(
modifier = Modifier.padding(horizontal = 16.dp),
color = MaterialTheme.lexicon.colorScheme.placeholder,
)
}
}
}

View file

@ -0,0 +1,77 @@
package com.pixelized.rplexicon.ui.screens.character.composable.common
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.material.Surface
import androidx.compose.material.minimumInteractiveComponentSize
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.pixelized.rplexicon.ui.theme.LexiconTheme
@Composable
fun CounterButton(
modifier: Modifier = Modifier,
value: Int?,
max: Int?,
onClick: (() -> Unit)? = null,
) {
Row(
modifier = modifier
.minimumInteractiveComponentSize()
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false),
enabled = onClick != null,
onClick = { onClick?.invoke() },
)
.then(other = modifier),
horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
) {
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary,
text = "${value ?: 0}",
)
max?.let {
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.bodySmall,
fontWeight = FontWeight.Light,
text = "/"
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Light,
text = "$it",
)
}
}
}
@Composable
@Preview(uiMode = UI_MODE_NIGHT_NO)
@Preview(uiMode = UI_MODE_NIGHT_YES)
private fun CounterPreview() {
LexiconTheme {
Surface {
CounterButton(
value = 1,
max = 2,
)
}
}
}

View file

@ -1,4 +1,4 @@
package com.pixelized.rplexicon.ui.screens.character.composable.actions
package com.pixelized.rplexicon.ui.screens.character.composable.common
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
@ -6,7 +6,7 @@ import androidx.annotation.DrawableRes
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.material.minimumInteractiveComponentSize
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@ -18,22 +18,19 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.ui.theme.LexiconTheme
@Composable
fun DiceButton(
modifier: Modifier = Modifier,
minWidth: Dp = 42.dp,
@DrawableRes icon: Int,
text: String,
onClick: () -> Unit,
) {
Column(
modifier = modifier
.sizeIn(minWidth = minWidth)
.minimumInteractiveComponentSize()
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false),

View file

@ -3,52 +3,51 @@ package com.pixelized.rplexicon.ui.screens.character.pages.actions
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ModalBottomSheetState
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.hilt.navigation.compose.hiltViewModel
import com.pixelized.rplexicon.LocalRollOverlay
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.ui.composable.edit.HandleCounterEditDialog
import com.pixelized.rplexicon.ui.composable.edit.HandleHitPointEditDialog
import com.pixelized.rplexicon.ui.composable.edit.HandleSkillEditDialog
import com.pixelized.rplexicon.ui.composable.edit.HandleSpellEditDialog
import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost
import com.pixelized.rplexicon.ui.navigation.screens.navigateToSpellDetail
import com.pixelized.rplexicon.ui.screens.character.composable.actions.Attack
import com.pixelized.rplexicon.ui.screens.character.composable.actions.AttackHeader
import com.pixelized.rplexicon.ui.screens.character.composable.actions.AttackUio
import com.pixelized.rplexicon.ui.screens.character.composable.actions.SkillHeader
import com.pixelized.rplexicon.ui.screens.character.composable.actions.SkillItem
import com.pixelized.rplexicon.ui.screens.character.composable.actions.SkillItemUio
import com.pixelized.rplexicon.ui.screens.character.composable.actions.Spell
import com.pixelized.rplexicon.ui.screens.character.composable.actions.SpellHeader
import com.pixelized.rplexicon.ui.screens.character.composable.actions.SpellHeaderUio
import com.pixelized.rplexicon.ui.screens.character.composable.actions.SpellUio
import com.pixelized.rplexicon.ui.screens.character.composable.actions.rememberTokenListStatePreview
import com.pixelized.rplexicon.ui.screens.character.composable.character.CharacterSheetHeader
import com.pixelized.rplexicon.ui.screens.character.composable.character.CharacterSheetHeaderUio
import com.pixelized.rplexicon.ui.screens.character.composable.preview.rememberAttackListStatePreview
import com.pixelized.rplexicon.ui.screens.character.composable.preview.rememberCharacterHeaderStatePreview
import com.pixelized.rplexicon.ui.screens.character.composable.preview.rememberSpellListStatePreview
import com.pixelized.rplexicon.ui.screens.character.pages.alteration.composable.TokenItem
import com.pixelized.rplexicon.ui.screens.character.pages.alteration.composable.TokenItemUio
import com.pixelized.rplexicon.ui.screens.character.pages.alteration.composable.rememberTokenListStatePreview
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.extentions.lexicon
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterialApi::class)
@ -56,9 +55,9 @@ import kotlinx.coroutines.launch
fun ActionPage(
sheetState: ModalBottomSheetState,
headerViewModel: HeaderViewModel = hiltViewModel(),
attackViewModel: AttackActionViewModel = hiltViewModel(),
spellsViewModel: SpellsActionViewModel = hiltViewModel(),
tokenViewModel: TokenViewModel = hiltViewModel(),
attacksViewModel: AttacksViewModel = hiltViewModel(),
spellsViewModel: SpellsViewModel = hiltViewModel(),
skillViewModel: SkillsViewModel = hiltViewModel(),
) {
val screen = LocalScreenNavHost.current
val overlay = LocalRollOverlay.current
@ -67,27 +66,34 @@ fun ActionPage(
ActionsPageContent(
modifier = Modifier.fillMaxWidth(),
header = headerViewModel.header,
attacks = attackViewModel.attacks,
tokens = tokenViewModel.tokens,
attacks = attacksViewModel.attacks,
tokens = skillViewModel.skills,
spells = spellsViewModel.spells,
onHitPoint = headerViewModel::toggleHitPointDialog,
onAttackHit = { id ->
attackViewModel.onHitRoll(id)?.let {
attacksViewModel.onHitRoll(id)?.let {
overlay.prepareRoll(diceThrow = it)
overlay.showOverlay()
}
},
onAttackDamage = { id ->
attackViewModel.onDamageRoll(id)?.let {
attacksViewModel.onDamageRoll(id)?.let {
overlay.prepareRoll(diceThrow = it)
overlay.showOverlay()
}
},
onToken = {
tokenViewModel.showTokenEditDialog(dialog = it)
onSkillCount = {
skillViewModel.showSkillEditDialog(item = it)
},
onSkillThrow = {
overlay.prepareRoll(diceThrow = skillViewModel.onSkillRoll(it.label))
overlay.showOverlay()
},
onSkillInfo = {
skillViewModel.showSkillDetailDialog(item = it)
},
onSpellLevel = { level: Int, value: Int, max: Int ->
tokenViewModel.showSpellTokenEditDialog(level = level, value = value, max = max)
spellsViewModel.showSpellEditDialog(level = level, value = value, max = max)
},
onSpell = { spell ->
screen.navigateToSpellDetail(
@ -120,10 +126,21 @@ fun ActionPage(
onConfirm = headerViewModel::applyHitPointChange,
)
HandleCounterEditDialog(
dialog = tokenViewModel.dialog,
onDismissRequest = tokenViewModel::hideCounterEditDialog,
onConfirm = tokenViewModel::applyCounterValue
HandleSpellEditDialog(
dialog = spellsViewModel.spellEditDialog,
onDismissRequest = spellsViewModel::hideSpellEditDialog,
onConfirm = spellsViewModel::applySpellChange
)
HandleSkillEditDialog(
dialog = skillViewModel.skillEditDialog,
onDismissRequest = skillViewModel::hideSkillEditDialog,
onConfirm = skillViewModel::applySkillChange
)
HandleSkillDetailDialog(
dialog = skillViewModel.skillDetailDialog,
onDismissRequest = skillViewModel::hideSkillDetailDialog
)
}
@ -134,96 +151,104 @@ fun ActionsPageContent(
lazyListState: LazyListState = rememberLazyListState(),
header: State<CharacterSheetHeaderUio?>,
attacks: State<List<AttackUio>>,
tokens: State<List<TokenItemUio>>,
tokens: State<List<SkillItemUio>>,
spells: State<List<Pair<SpellHeaderUio, List<SpellUio>>>>,
onHitPoint: () -> Unit,
onAttackHit: (id: String) -> Unit,
onAttackDamage: (id: String) -> Unit,
onToken: (TokenItemUio) -> Unit,
onSkillThrow: (SkillItemUio) -> Unit,
onSkillCount: (SkillItemUio) -> Unit,
onSkillInfo: (SkillItemUio) -> Unit,
onSpellLevel: (level: Int, value: Int, max: Int) -> Unit,
onSpell: (id: String) -> Unit,
onSpellHit: (id: String) -> Unit,
onSpellDamage: (id: String) -> Unit,
onCast: (id: String) -> Unit,
) {
LazyColumn(
Column(
modifier = modifier,
state = lazyListState,
contentPadding = PaddingValues(bottom = 16.dp),
) {
stickyHeader {
CharacterSheetHeader(
modifier = Modifier.fillMaxWidth(),
header = header,
onHitPoint = onHitPoint,
CharacterSheetHeader(
modifier = Modifier.fillMaxWidth(),
header = header,
onHitPoint = onHitPoint,
)
LazyColumn(
state = lazyListState,
) {
if (attacks.value.isNotEmpty()) {
stickyHeader {
AttackHeader()
}
items(items = attacks.value) {
Attack(
weapon = it,
onHit = onAttackHit,
onDamage = onAttackDamage,
)
}
items(count = 1) {
Spacer(modifier = Modifier.height(height = 16.dp))
}
}
if (tokens.value.isNotEmpty()) {
stickyHeader {
SkillHeader()
}
items(items = tokens.value) {
SkillItem(
skill = it,
onInfo = onSkillInfo,
onThrow = onSkillThrow,
onSkill = onSkillCount,
)
}
items(count = 1) {
Spacer(modifier = Modifier.height(height = 16.dp))
}
}
spells.value.forEach { entry ->
stickyHeader {
SpellHeader(
header = entry.first,
onSpell = onSpellLevel,
)
}
items(items = entry.second) {
Spell(
spell = it,
onClick = onSpell,
onHit = onSpellHit,
onEffect = onSpellDamage,
onCast = onCast,
)
}
items(count = 1) {
Spacer(modifier = Modifier.height(height = 16.dp))
}
}
}
}
}
@Composable
private fun HandleSkillDetailDialog(
dialog: State<SkillDetailUio?>,
onDismissRequest: () -> Unit,
) {
dialog.value?.let {
Dialog(
properties = remember { DialogProperties(usePlatformDefaultWidth = false) },
onDismissRequest = onDismissRequest,
) {
SkillDetail(
detail = it,
onClose = onDismissRequest,
)
}
items(count = 1) {
Spacer(modifier = Modifier.height(height = 8.dp))
}
items(items = attacks.value) {
Attack(
weapon = it,
onHit = onAttackHit,
onDamage = onAttackDamage,
)
}
if (tokens.value.isNotEmpty()) {
items(count = 1) {
Spacer(modifier = Modifier.height(height = 16.dp))
}
stickyHeader {
Text(
modifier = Modifier.padding(horizontal = 16.dp),
style = MaterialTheme.typography.titleMedium,
text = stringResource(id = R.string.token_label_title).let {
AnnotatedString(
text = it,
spanStyles = listOf(
AnnotatedString.Range(
item = MaterialTheme.lexicon.typography.bodyDropCapSpan,
start = 0,
end = Integer.min(1, it.length),
)
)
)
}
)
}
items(items = tokens.value) {
TokenItem(
counter = it,
onClick = onToken,
)
}
}
spells.value.forEach { entry ->
items(count = 1) {
Spacer(modifier = Modifier.height(height = 8.dp))
}
stickyHeader {
SpellHeader(
header = entry.first,
onSpell = onSpellLevel,
)
}
items(count = 1) {
Spacer(modifier = Modifier.height(height = 8.dp))
}
items(items = entry.second) {
Spell(
spell = it,
onClick = onSpell,
onHit = onSpellHit,
onEffect = onSpellDamage,
onCast = onCast,
)
}
}
}
}
@ -242,7 +267,9 @@ fun ActionPagePreview() {
onHitPoint = { },
onAttackHit = { },
onAttackDamage = { },
onToken = { },
onSkillCount = { },
onSkillThrow = { },
onSkillInfo = { },
onSpellLevel = { _, _, _ -> },
onSpell = { },
onSpellHit = { },

View file

@ -23,7 +23,7 @@ import kotlinx.coroutines.withContext
import javax.inject.Inject
@HiltViewModel
class AttackActionViewModel @Inject constructor(
class AttacksViewModel @Inject constructor(
application: Application,
savedStateHandle: SavedStateHandle,
private val characterRepository: CharacterSheetRepository,

View file

@ -0,0 +1,140 @@
package com.pixelized.rplexicon.ui.screens.character.pages.actions
import android.content.res.Configuration
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
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.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
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.text.AnnotatedString
import androidx.compose.ui.text.font.FontStyle
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.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 SkillDetailUio(
val name: String,
val original: String?,
val description: String,
)
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun SkillDetail(
modifier: Modifier = Modifier,
detail: SkillDetailUio,
onClose: () -> Unit,
) {
Surface(
modifier = Modifier
.padding(all = 16.dp)
.ddBorder(
inner = remember { RoundedCornerShape(size = 8.dp) },
outline = remember { CutCornerShape(size = 16.dp) },
)
.then(other = modifier),
) {
Column {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(start = 24.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Top,
) {
FlowRow(
modifier = Modifier
.padding(top = 16.dp)
.weight(weight = 1f),
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.titleMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
text = AnnotatedString(
text = detail.name,
spanStyles = listOf(
AnnotatedString.Range(
item = MaterialTheme.lexicon.typography.bodyDropCapSpan,
start = 0,
end = Integer.min(1, detail.name.length),
)
)
),
)
detail.original?.let {
Text(
modifier = Modifier.alignByBaseline(),
fontWeight = FontWeight.Light,
fontStyle = FontStyle.Italic,
style = MaterialTheme.typography.labelSmall,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
text = it,
)
}
}
IconButton(onClick = onClose) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = null
)
}
}
Column(
modifier = Modifier
.padding(top = 8.dp)
.verticalScroll(rememberScrollState())
.padding(horizontal = 24.dp),
) {
Text(
modifier = Modifier.padding(top = 8.dp, bottom = 24.dp),
style = MaterialTheme.typography.bodyMedium,
text = detail.description,
)
}
}
}
}
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun AlterationDetailPreview() {
LexiconTheme {
SkillDetail(
detail = SkillDetailUio(
name = "Endurance implacable",
original = "Relentless Endurance",
description = "Lorsque vous tombez à 0 point de vie, mais que vous n'êtes pas tué sur le coup, vous pouvez passer à 1 point de vie à la place. Vous devez terminer un repos long avant de pouvoir utiliser cette capacité de nouveau."
),
onClose = { },
)
}
}

View file

@ -0,0 +1,119 @@
package com.pixelized.rplexicon.ui.screens.character.pages.actions
import android.app.Application
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.model.DiceThrow
import com.pixelized.rplexicon.repository.authentication.FirebaseRepository
import com.pixelized.rplexicon.repository.data.DescriptionRepository
import com.pixelized.rplexicon.repository.data.SkillRepository
import com.pixelized.rplexicon.ui.composable.edit.SkillEditDialogUio
import com.pixelized.rplexicon.ui.navigation.screens.characterSheetArgument
import com.pixelized.rplexicon.ui.screens.character.composable.actions.SkillItemUio
import com.pixelized.rplexicon.utilitary.extentions.icon
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
@HiltViewModel
class SkillsViewModel @Inject constructor(
private val skillRepository: SkillRepository,
private val firebaseRepository: FirebaseRepository,
private val descriptionRepository: DescriptionRepository,
application: Application,
savedStateHandle: SavedStateHandle,
) : AndroidViewModel(application) {
private val character = savedStateHandle.characterSheetArgument.name
private val _skillEditDialog = mutableStateOf<SkillEditDialogUio?>(null)
val skillEditDialog: State<SkillEditDialogUio?> get() = _skillEditDialog
private val _skillDetailDialog = mutableStateOf<SkillDetailUio?>(null)
val skillDetailDialog: State<SkillDetailUio?> get() = _skillDetailDialog
private val _skills = mutableStateOf<List<SkillItemUio>>(emptyList())
val skills: State<List<SkillItemUio>> get() = _skills
init {
viewModelScope.launch {
launch(Dispatchers.IO) {
skillRepository.skills
.combine(firebaseRepository.getCharacter(character = character)) { sheets, fire ->
sheets[character] to fire
}
.collect { data ->
val (values, fire) = data
val skills = values?.map { skill ->
val description = descriptionRepository.find(name = skill.name)
SkillItemUio(
label = skill.name,
translate = description?.original,
rest = skill.rest,
cost = skill.cost,
effect = skill.effect?.let {
SkillItemUio.Dice(
icon = it.faces.icon,
label = "${it.amount}d${it.faces}",
)
},
value = fire.skills[skill.name],
max = skill.amount,
haveDetail = description?.description != null,
)
} ?: emptyList()
withContext(Dispatchers.Main) {
_skills.value = skills
}
}
}
}
}
fun onSkillRoll(name: String): DiceThrow = DiceThrow.Skill(
character = character,
skill = name,
)
fun showSkillDetailDialog(item: SkillItemUio) {
_skillDetailDialog.value = descriptionRepository
.find(name = item.label)
?.let { description ->
SkillDetailUio(
name = item.label,
original = description.original,
description = description.description
)
}
}
fun hideSkillDetailDialog() {
_skillDetailDialog.value = null
}
fun showSkillEditDialog(item: SkillItemUio) {
if (item.max != null) {
_skillEditDialog.value = SkillEditDialogUio(
label = item.label,
value = item.value ?: 0,
max = item.max,
)
}
}
fun hideSkillEditDialog() {
_skillEditDialog.value = null
}
fun applySkillChange(id: String, value: Int) {
firebaseRepository.setSkill(character = character, name = id, value = value)
hideSkillEditDialog()
}
}

View file

@ -16,6 +16,7 @@ import com.pixelized.rplexicon.model.Throw
import com.pixelized.rplexicon.repository.authentication.FirebaseRepository
import com.pixelized.rplexicon.repository.data.CharacterSheetRepository
import com.pixelized.rplexicon.repository.data.SpellRepository
import com.pixelized.rplexicon.ui.composable.edit.SpellEditDialogUio
import com.pixelized.rplexicon.ui.navigation.screens.characterSheetArgument
import com.pixelized.rplexicon.ui.screens.character.composable.actions.SpellHeaderUio
import com.pixelized.rplexicon.ui.screens.character.composable.actions.SpellLevelUio
@ -36,7 +37,7 @@ import javax.inject.Inject
import kotlin.math.max
@HiltViewModel
class SpellsActionViewModel @Inject constructor(
class SpellsViewModel @Inject constructor(
private val characterRepository: CharacterSheetRepository,
private val firebaseRepository: FirebaseRepository,
private val spellRepository: SpellRepository,
@ -48,6 +49,9 @@ class SpellsActionViewModel @Inject constructor(
private var character: CharacterSheet? = null
private var characterFire: CharacterSheetFire? = null
private val _editDialog = mutableStateOf<SpellEditDialogUio?>(null)
val spellEditDialog: State<SpellEditDialogUio?> get() = _editDialog
private val _spells = mutableStateOf<List<Pair<SpellHeaderUio, List<SpellUio>>>>(emptyList())
val spells: State<List<Pair<SpellHeaderUio, List<SpellUio>>>> get() = _spells
@ -138,7 +142,7 @@ class SpellsActionViewModel @Inject constructor(
character = characterName,
spell = name,
)
return when (character?.isWarlock?: false) {
return when (character?.isWarlock ?: false) {
true -> false
else -> (character?.highestSpellLevel() ?: 1) > (assignedSpell?.spell?.level ?: 1)
}
@ -202,6 +206,23 @@ class SpellsActionViewModel @Inject constructor(
return DiceThrow.SpellDamage(character = characterName, spell = id)
}
fun showSpellEditDialog(level: Int, value: Int, max: Int) {
_editDialog.value = SpellEditDialogUio(
level = level,
value = value,
max = max,
)
}
fun hideSpellEditDialog() {
_editDialog.value = null
}
fun applySpellChange(level: Int, value: Int) {
firebaseRepository.setSpell(character = characterName, level = level, value = value)
hideSpellEditDialog()
}
/**
* Helper method to build a readable String from a Throw.
* Create a string following the format "amount 'd' faces '+' modifiers + amount * level 'd' faces"

View file

@ -1,129 +0,0 @@
package com.pixelized.rplexicon.ui.screens.character.pages.actions
import android.app.Application
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.model.CharacterSheetFire
import com.pixelized.rplexicon.repository.authentication.FirebaseRepository
import com.pixelized.rplexicon.repository.data.AlterationRepository
import com.pixelized.rplexicon.repository.data.CharacterSheetRepository
import com.pixelized.rplexicon.ui.composable.edit.CounterEditDialogUio
import com.pixelized.rplexicon.ui.navigation.screens.characterSheetArgument
import com.pixelized.rplexicon.ui.screens.character.pages.alteration.composable.TokenItemUio
import com.pixelized.rplexicon.utilitary.extentions.context
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
@HiltViewModel
class TokenViewModel @Inject constructor(
private val characterRepository: CharacterSheetRepository,
private val firebaseRepository: FirebaseRepository,
private val alterationRepository: AlterationRepository,
application: Application,
savedStateHandle: SavedStateHandle,
) : AndroidViewModel(application) {
private val character = savedStateHandle.characterSheetArgument.name
private val _dialog = mutableStateOf<CounterEditDialogUio?>(null)
val dialog: State<CounterEditDialogUio?> get() = _dialog
private val _counters = mutableStateOf<List<TokenItemUio>>(emptyList())
val tokens: State<List<TokenItemUio>> get() = _counters
init {
viewModelScope.launch {
launch(Dispatchers.IO) {
characterRepository.data
.combine(firebaseRepository.getCharacter(character = character)) { sheets, fire ->
sheets.getValue(character) to fire
}
.collect { data ->
val (character, fire) = data
val counters = mutableListOf<TokenItemUio>()
character.rage?.let {
counters.add(
TokenItemUio(
id = CharacterSheetFire.RAGE,
icon = R.drawable.ic_fist_24,
label = R.string.token_label_rage,
value = fire.rage ?: 0,
max = it,
)
)
}
character.relentlessEndurance?.let {
counters.add(
TokenItemUio(
id = CharacterSheetFire.RELENTLESS_ENDURANCE,
icon = R.drawable.ic_burning_passion_24,
label = R.string.token_label_relentless_endurance,
value = fire.relentlessEndurance ?: 0,
max = it,
)
)
}
character.bardicInspiration?.let {
counters.add(
TokenItemUio(
id = CharacterSheetFire.BARDIC_INSPIRATION,
icon = R.drawable.ic_lyre_24,
label = R.string.token_label_bardic_inspiration,
value = fire.bardicInspiration ?: 0,
max = it,
)
)
}
character.divineConduit?.let {
counters.add(
TokenItemUio(
id = CharacterSheetFire.DIVINE_CONDUIT,
icon = R.drawable.ic_embrassed_energy_24,
label = R.string.token_label_divine_conduit,
value = fire.divineConduit ?: 0,
max = it,
)
)
}
withContext(Dispatchers.Main) {
_counters.value = counters
}
}
}
}
}
fun showTokenEditDialog(dialog: TokenItemUio) {
_dialog.value = CounterEditDialogUio(
id = dialog.id,
label = context.getString(dialog.label),
value = dialog.value,
max = dialog.max,
)
}
fun showSpellTokenEditDialog(level: Int, value: Int, max: Int) {
_dialog.value = CounterEditDialogUio(
id = CharacterSheetFire.SPELL_SLOT_LEVEL_X + level,
label = context.getString(R.string.spell_level_chooser_label, "$level"),
value = value,
max = max,
)
}
fun hideCounterEditDialog() {
_dialog.value = null
}
fun applyCounterValue(id: String, value: Int) {
firebaseRepository.setToken(character = character, token = id, value = value)
_dialog.value = null
}
}

View file

@ -1,4 +1,4 @@
package com.pixelized.rplexicon.ui.screens.rolls.composable
package com.pixelized.rplexicon.ui.screens.character.pages.alteration
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES

View file

@ -17,10 +17,8 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.hilt.navigation.compose.hiltViewModel
import com.pixelized.rplexicon.ui.screens.rolls.composable.AlterationDetail
import com.pixelized.rplexicon.ui.screens.rolls.composable.AlterationDetailUio
import com.pixelized.rplexicon.ui.screens.rolls.composable.RollAlteration
import com.pixelized.rplexicon.ui.screens.rolls.composable.RollAlterationUio
import com.pixelized.rplexicon.ui.screens.character.composable.actions.AlterationItem
import com.pixelized.rplexicon.ui.screens.character.composable.actions.AlterationItemUio
import com.pixelized.rplexicon.ui.screens.rolls.preview.rememberRollAlterations
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import kotlinx.coroutines.launch
@ -53,7 +51,7 @@ fun AlterationPage(
@Composable
fun AlterationPageContent(
modifier: Modifier = Modifier,
alterations: State<List<RollAlterationUio>>,
alterations: State<List<AlterationItemUio>>,
onAlterationInfo: (String) -> Unit,
onAlterationClick: (String) -> Unit,
) {
@ -62,7 +60,7 @@ fun AlterationPageContent(
contentPadding = PaddingValues(top = 8.dp, bottom = 16.dp),
) {
items(items = alterations.value) {
RollAlteration(
AlterationItem(
modifier = Modifier.fillMaxWidth(),
alteration = it,
onInfo = onAlterationInfo,

View file

@ -10,8 +10,7 @@ import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.repository.data.AlterationRepository
import com.pixelized.rplexicon.repository.data.DescriptionRepository
import com.pixelized.rplexicon.ui.navigation.screens.characterSheetArgument
import com.pixelized.rplexicon.ui.screens.rolls.composable.AlterationDetailUio
import com.pixelized.rplexicon.ui.screens.rolls.composable.RollAlterationUio
import com.pixelized.rplexicon.ui.screens.character.composable.actions.AlterationItemUio
import com.pixelized.rplexicon.ui.screens.rolls.factory.AlterationFactory
import com.pixelized.rplexicon.utilitary.extentions.context
import dagger.hilt.android.lifecycle.HiltViewModel
@ -30,8 +29,8 @@ class AlterationViewModel @Inject constructor(
) : AndroidViewModel(application) {
private val character = savedStateHandle.characterSheetArgument.name
private val _alterations = mutableStateOf<List<RollAlterationUio>>(emptyList())
val alterations: State<List<RollAlterationUio>> get() = _alterations
private val _alterations = mutableStateOf<List<AlterationItemUio>>(emptyList())
val alterations: State<List<AlterationItemUio>> get() = _alterations
private val _alterationDetail = mutableStateOf<AlterationDetailUio?>(null)
val alterationDetail: State<AlterationDetailUio?> get() = _alterationDetail

View file

@ -1,156 +0,0 @@
package com.pixelized.rplexicon.ui.screens.character.pages.alteration.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.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.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
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.unit.dp
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.ui.theme.LexiconTheme
@Stable
data class TokenItemUio(
val id: String,
@DrawableRes val icon: Int,
@StringRes val label: Int,
val value: Int,
val max: Int,
)
@Composable
fun TokenItem(
modifier: Modifier = Modifier,
padding: PaddingValues = PaddingValues(horizontal = 16.dp),
counter: TokenItemUio,
onClick: (TokenItemUio) -> Unit,
) {
Box(
modifier = Modifier
.clickable { onClick(counter) }
.heightIn(min = 52.dp)
.padding(paddingValues = padding)
.then(other = modifier),
contentAlignment = Alignment.Center,
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(space = 16.dp),
) {
Icon(
painter = painterResource(id = counter.icon),
contentDescription = null,
)
Text(
modifier = Modifier
.alignByBaseline()
.weight(weight = 1f),
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Bold,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = stringResource(id = counter.label),
)
Row(
modifier = Modifier.alignByBaseline(),
horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
) {
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary,
text = "${counter.value}",
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.bodySmall,
fontWeight = FontWeight.Light,
text = "/"
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Light,
text = "${counter.max}",
)
}
}
}
}
@Composable
@Stable
fun rememberTokenListStatePreview(): State<List<TokenItemUio>> = remember {
mutableStateOf(
listOf(
TokenItemUio(
id = "1",
icon = R.drawable.ic_fist_24,
label = R.string.token_label_rage,
value = 2,
max = 2,
),
TokenItemUio(
id = "2",
icon = R.drawable.ic_embrassed_energy_24,
label = R.string.token_label_divine_conduit,
value = 2,
max = 4,
),
TokenItemUio(
id = "3",
icon = R.drawable.ic_lyre_24,
label = R.string.token_label_bardic_inspiration,
value = 2,
max = 3,
),
)
)
}
@Composable
@Preview(uiMode = UI_MODE_NIGHT_NO)
@Preview(uiMode = UI_MODE_NIGHT_YES)
private fun CounterItemPreview() {
LexiconTheme {
Surface {
TokenItem(
counter = TokenItemUio(
id = "0",
icon = R.drawable.ic_burning_passion_24,
label = R.string.token_label_relentless_endurance,
value = 2,
max = 2,
),
onClick = { },
)
}
}
}

View file

@ -67,10 +67,10 @@ import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.model.DiceThrow
import com.pixelized.rplexicon.ui.composable.BlurredOverlayHostState
import com.pixelized.rplexicon.ui.composable.ModalNavigationDrawer
import com.pixelized.rplexicon.ui.screens.rolls.composable.AlterationDetail
import com.pixelized.rplexicon.ui.screens.rolls.composable.AlterationDetailUio
import com.pixelized.rplexicon.ui.screens.rolls.composable.RollAlteration
import com.pixelized.rplexicon.ui.screens.rolls.composable.RollAlterationUio
import com.pixelized.rplexicon.ui.screens.character.pages.alteration.AlterationDetail
import com.pixelized.rplexicon.ui.screens.character.pages.alteration.AlterationDetailUio
import com.pixelized.rplexicon.ui.screens.character.composable.actions.AlterationItem
import com.pixelized.rplexicon.ui.screens.character.composable.actions.AlterationItemUio
import com.pixelized.rplexicon.ui.screens.rolls.composable.RollDice
import com.pixelized.rplexicon.ui.screens.rolls.composable.RollDiceUio
import com.pixelized.rplexicon.ui.screens.rolls.composable.ThrowsCard
@ -136,7 +136,7 @@ private fun RollOverlayContent(
drawer: DrawerState,
dice: State<RollDiceUio?>,
card: State<ThrowsCardUio?>,
alterations: State<List<RollAlterationUio>>,
alterations: State<List<AlterationItemUio>>,
showDetail: State<Boolean>,
onMenu: () -> Unit,
onMenuClose: () -> Unit,
@ -185,7 +185,7 @@ private fun RollOverlayContent(
contentPadding = PaddingValues(vertical = 8.dp),
) {
items(items = alterations.value) {
RollAlteration(
AlterationItem(
modifier = Modifier.fillMaxWidth(),
alteration = it,
onInfo = onAlterationInfo,

View file

@ -10,8 +10,8 @@ import com.pixelized.rplexicon.business.DiceThrowUseCase
import com.pixelized.rplexicon.model.DiceThrow
import com.pixelized.rplexicon.repository.data.AlterationRepository
import com.pixelized.rplexicon.repository.data.DescriptionRepository
import com.pixelized.rplexicon.ui.screens.rolls.composable.AlterationDetailUio
import com.pixelized.rplexicon.ui.screens.rolls.composable.RollAlterationUio
import com.pixelized.rplexicon.ui.screens.character.pages.alteration.AlterationDetailUio
import com.pixelized.rplexicon.ui.screens.character.composable.actions.AlterationItemUio
import com.pixelized.rplexicon.ui.screens.rolls.composable.RollDiceUio
import com.pixelized.rplexicon.ui.screens.rolls.composable.ThrowsCardUio
import com.pixelized.rplexicon.ui.screens.rolls.factory.AlterationFactory
@ -36,8 +36,8 @@ class RollOverlayViewModel @Inject constructor(
private var diceThrow: DiceThrow? = null
private var rollJob: Job? = null
private val _alterations = mutableStateOf<List<RollAlterationUio>>(emptyList())
val alterations: State<List<RollAlterationUio>> get() = _alterations
private val _alterations = mutableStateOf<List<AlterationItemUio>>(emptyList())
val alterations: State<List<AlterationItemUio>> get() = _alterations
private val _dice = mutableStateOf<RollDiceUio?>(null)
val dice: State<RollDiceUio?> get() = _dice

View file

@ -5,18 +5,20 @@ import com.pixelized.rplexicon.model.DiceThrow
import com.pixelized.rplexicon.model.Property
import com.pixelized.rplexicon.repository.data.ActionRepository
import com.pixelized.rplexicon.repository.data.AlterationRepository
import com.pixelized.rplexicon.repository.data.SkillRepository
import com.pixelized.rplexicon.repository.data.SpellRepository
import com.pixelized.rplexicon.ui.screens.rolls.composable.RollAlterationUio
import com.pixelized.rplexicon.ui.screens.character.composable.actions.AlterationItemUio
import javax.inject.Inject
class AlterationFactory @Inject constructor(
private val actionRepository: ActionRepository,
private val spellRepository: SpellRepository,
private val skillRepository: SkillRepository,
private val alterationRepository: AlterationRepository,
) {
fun convert(character: String, alterations: List<Alteration>): List<RollAlterationUio> {
fun convert(character: String, alterations: List<Alteration>): List<AlterationItemUio> {
return alterations.map {
RollAlterationUio(
AlterationItemUio(
label = it.name,
checked = alterationRepository.getStatus(character, it.name),
subLabel = it.source,
@ -24,7 +26,7 @@ class AlterationFactory @Inject constructor(
}
}
fun convertDiceThrow(diceThrow: DiceThrow): List<RollAlterationUio> {
fun convertDiceThrow(diceThrow: DiceThrow): List<AlterationItemUio> {
val properties = when (diceThrow) {
is DiceThrow.Initiative -> listOf(Property.INITIATIVE, Property.DEXTERITY)
is DiceThrow.Strength -> listOf(Property.STRENGTH, Property.STRENGTH_THROW)
@ -91,12 +93,17 @@ class AlterationFactory @Inject constructor(
val spell = spellRepository.find(diceThrow.character, spell = diceThrow.spell)
spell?.effect?.modifier ?: emptyList()
}
is DiceThrow.Skill -> {
val skill = skillRepository.find(diceThrow.character, skill = diceThrow.skill)
skill?.effect?.modifier ?: emptyList()
}
}
return alterationRepository
.getAlterations(character = diceThrow.character, *properties.toTypedArray())
.map {
RollAlterationUio(
AlterationItemUio(
label = it.name,
checked = alterationRepository.getStatus(diceThrow.character, it.name),
subLabel = it.source,

View file

@ -4,29 +4,29 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import com.pixelized.rplexicon.ui.screens.rolls.composable.RollAlterationUio
import com.pixelized.rplexicon.ui.screens.character.composable.actions.AlterationItemUio
@Composable
@Stable
fun rememberRollAlterations() = remember {
mutableStateOf(
listOf(
RollAlterationUio(
AlterationItemUio(
label = "Rage",
subLabel = "Barbare",
checked = false,
),
RollAlterationUio(
AlterationItemUio(
label = "Inspiration bardique",
subLabel = "Barde",
checked = false
),
RollAlterationUio(
AlterationItemUio(
label = "Bénédiction",
subLabel = "Clerc",
checked = false,
),
RollAlterationUio(
AlterationItemUio(
label = "Cape de protection",
subLabel = "Équipement",
checked = true

View file

@ -16,18 +16,7 @@ fun CharacterSheet.spell(level: Int): Int? = when (level) {
else -> null
}
fun CharacterSheetFire.spell(level: Int): Int? = when (level) {
1 -> spell1
2 -> spell2
3 -> spell3
4 -> spell4
5 -> spell5
6 -> spell6
7 -> spell7
8 -> spell8
9 -> spell9
else -> null
}
fun CharacterSheetFire.spell(level: Int): Int? = spells["${CharacterSheetFire.SPELL_PREFIX}$level"]
fun CharacterSheet.highestSpellLevel(): Int = when {
spell9 != null -> 9

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:fillColor="#000000"
android:pathData="M19.84,18.75v2.72c64.27,50.96 95.31,115.85 89.44,179.25 -10.6,-55 -41.76,-104.8 -89.44,-138.91v23.53c55.24,45.18 82.41,114.31 72.97,185.25 -0.4,2.33 -0.76,4.66 -1.06,7 -0.09,0.51 -0.16,1.02 -0.25,1.53h0.06c-6.55,53.8 11.2,108.57 49.59,156.03 6.41,11.07 13.98,21.8 22.69,32.13C95.25,406.66 59.08,335.53 53.22,262.41c-11.11,83 15.11,163.21 90.69,230.22L188.5,492.63c0.03,0.03 0.06,0.06 0.09,0.09h130.47c0.03,-0.03 0.06,-0.06 0.09,-0.09h43.63c75.58,-67.01 101.8,-147.22 90.69,-230.22 -5.75,71.77 -40.7,141.62 -106.85,201.5 9.56,-11.75 17.68,-24.02 24.28,-36.69 34.1,-45.58 49.6,-97.28 43.41,-148.1 -0,-0.02 0,-0.04 0,-0.06 -0.41,-3.31 -0.91,-6.61 -1.5,-9.91 -9.36,-74.25 21.31,-146.35 82.31,-190.88L495.13,55.63c-52.75,34.07 -87.21,86.5 -98.53,144.84 -5.85,-64.21 26.1,-129.92 92.12,-181.13l-0.47,-0.59h-28.06c-72.05,64.34 -99.85,149.67 -72.5,228.06 2.89,8.29 5.11,16.68 6.66,25.09 0,0.04 0.03,0.08 0.03,0.13 0.44,3.43 0.93,6.88 1.53,10.31h0.03c2.3,19.37 1.12,38.89 -3.4,58.16 -0.04,-28.38 -6.78,-57.15 -20.44,-85.06 -40.06,-81.86 -20.77,-171.43 52.41,-236.69h-31.03c-50.15,46.62 -66.32,91.56 -57.44,151.09 -21.49,-59.17 -19.42,-103.58 20.69,-151.09L152,18.75c40.1,47.51 42.18,91.93 20.69,151.09 8.89,-59.53 -7.27,-104.47 -57.41,-151.09L83.16,18.75c73.17,65.26 92.46,154.82 52.41,236.69 -14.9,30.45 -21.52,61.92 -20.25,92.78 -6.15,-21.75 -8.02,-43.91 -5.41,-65.87 0.6,-3.44 1.12,-6.88 1.56,-10.31 1.55,-8.46 3.78,-16.88 6.69,-25.22 27.35,-78.39 -0.45,-163.72 -72.5,-228.06L19.84,18.75zM254.09,38.44c16.4,0 27.02,6.18 34.72,16.59 7.69,10.41 11.97,25.73 11.97,43 0,18.66 -6.89,38.56 -15.97,49.5l-10.13,12.22 15.59,2.94c12.52,2.35 21.72,8.77 29.44,19 7.72,10.23 13.57,24.36 17.69,40.69 7.52,29.84 9.14,66.52 9.38,99.34h-23.31l-0.81,-70.5 -18.69,0.22 0.97,86.44 -7.75,111.63c47.06,-43.67 71.99,-94.3 76.16,-146.31 8.21,61.34 -11.15,120.61 -67,170.13L295.53,473.31v0.22h-32.94L262.59,333.81h-18.69v139.72L212.62,473.53v-0.22h-9.65c-55.85,-49.52 -75.24,-108.79 -67.03,-170.13 4.13,51.56 28.66,101.78 74.94,145.19L203.78,345.72l2.6,-94.13 -18.69,-0.53 -1.94,70.65h-24.38c0.24,-32.83 1.88,-69.5 9.41,-99.35 4.12,-16.33 9.97,-30.46 17.69,-40.69 7.72,-10.23 16.92,-16.65 29.44,-19l15.59,-2.94 -10.13,-12.22c-9.08,-10.94 -15.97,-30.83 -15.97,-49.5 0,-17.27 4.28,-32.59 11.97,-43 7.69,-10.41 18.32,-16.59 34.72,-16.59z" />
</vector>

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:fillColor="#000000"
android:pathData="M179.81,20.72v81.25L135.78,75.62l17.56,46.94 -115.66,-20.94 84.72,49.91H20v27.34l110.47,14.88 96.59,-29.19c-11.3,-11.87 -18.59,-30.74 -18.59,-52 0,-35.93 20.87,-65.06 46.62,-65.06 25.75,0 46.63,29.14 46.63,65.06 0,20.85 -7.04,39.38 -17.97,51.28l99.03,29.91 112.5,-15.16V151.53H394.19l84.72,-49.9 -120.44,21.78 17.87,-47.72 -48.66,29.13V20.72H179.81zM495.28,223.34l-112.5,22.44 -55.4,-13.12 -28.03,118.31 16.59,145h51.69L329.25,351.22l46.53,27.84 -21.31,-56.94 124.44,22.5 -91.13,-53.69h107.5v-67.59zM20,223.75v67.19h108.81l-91.13,53.69L157.31,322.97 136.35,379l38.47,-23 -28.59,139.97h48.15L207.28,351.56 185.6,232.72l-55.13,13.06L20,223.75z" />
</vector>

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:fillColor="#000000"
android:pathData="M227.23,21.78c-1.85,0 -3.7,0.05 -5.57,0.16 -15.31,0.88 -30.76,5.3 -39.49,10.86l-0.01,73.15c2.88,-0.09 5.78,-0.15 8.68,-0.14 23.38,0.04 47.1,3.29 68.47,9.51l0.01,-87.51c-7.03,-3.52 -19.18,-6.03 -32.09,-6.03zM307.97,30.94c-11.93,0.15 -23.08,2.36 -29.97,5.6l-0.01,77.6v7.66c38.49,15.67 64.81,42.48 58.74,78.76l-0.96,5.73 -5.56,1.67c-17.45,5.25 -34.87,9.7 -52.22,13.34L277.98,246.53c25.56,-0.7 51.33,-2.69 77.14,-6.1l0.02,-197.93c-8.28,-5.56 -23.51,-10.24 -38.84,-11.33 -2.79,-0.2 -5.58,-0.27 -8.34,-0.24zM143.22,46.29c-1.18,-0.01 -2.37,-0.01 -3.59,0.02 -4.18,0.1 -8.53,0.47 -12.9,1.15 -15.67,2.45 -31.48,8.56 -40.41,15.4l-0.01,72.96c18.81,-15.81 46.7,-25.14 77.15,-28.54l0.01,-57.97c-4.82,-1.75 -12.02,-2.92 -20.25,-3.02zM401.62,49.75c-10.8,0.12 -20.72,1.93 -27.04,4.66l-0.02,183.18c25.07,-4.02 50.16,-9.41 75.12,-16.36l1.99,-158.45c-8.35,-5.9 -23.65,-11.02 -39.05,-12.55 -3.7,-0.37 -7.4,-0.52 -11,-0.48zM178.84,123.96c-53.72,0.7 -101.41,20.36 -97.89,66.6 15.84,-3.92 30.84,-5.89 44.94,-6.1 34.84,-0.51 64.21,9.7 87.32,27.61 34.61,-3.11 69.85,-10 105.41,-20.31 0.14,-41.29 -74.1,-68.66 -139.78,-67.8zM129.97,202.61c-1.3,-0 -2.6,0.01 -3.92,0.05 -17.26,0.44 -36.45,4.03 -57.57,11.04 5.79,53.81 26.33,106.41 58.5,143.35 6.23,7.15 12.86,13.71 19.88,19.61 29.3,9.28 69.26,12.92 110.53,12.14 3.78,-55.81 -8.72,-108.36 -36.19,-142.74 -21.26,-26.61 -51.06,-43.39 -91.23,-43.44zM259.29,224.89c-9.36,1.64 -18.69,3.02 -28,4.15 1.54,1.74 3.04,3.52 4.5,5.35 3.15,3.94 6.09,8.06 8.87,12.33 9.92,0.14 19.87,0.13 29.86,-0.11L259.29,246.61v-21.72zM451.11,240.23c-65.41,17.83 -131.46,25.41 -195.85,25.32 17,35.14 23.83,78.09 21.01,122.6 42.48,-2.08 85.03,-8.23 118.19,-15.98 26.69,-32.78 47.37,-77.12 56.65,-131.93zM400.51,389.9c-38.33,9.15 -87.95,16.06 -136.87,17.45 -47.67,1.36 -94.34,-2.23 -129.45,-15.26l-0.01,78.93c27.19,12.57 76.41,20.2 127.32,20.3 51.22,0.09 104.21,-7.17 139,-20.77l0.01,-80.65z" />
</vector>

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:fillColor="#000000"
android:pathData="M108.66,35.06c-15.05,0.14 -33.41,5.38 -46.97,15.81 -10.75,8.28 -18.78,19.27 -21.19,34.44 0.21,-0.13 0.41,-0.25 0.63,-0.38 -0.84,2.82 -1.31,5.79 -1.31,8.88 0,17.09 13.85,30.94 30.94,30.94 14.29,0 26.32,-9.7 29.88,-22.88 0.03,-0.12 0.07,-0.23 0.09,-0.34 0.48,-2.08 0.77,-4.04 0.9,-5.84 0.03,-0.33 0.02,-0.65 0.03,-0.97 0,-0.13 0.03,-0.25 0.03,-0.38 0.01,-0.18 -0,-0.35 0,-0.53 0,-1.53 -0.1,-3.04 -0.31,-4.5 -1.37,-8.02 -6.78,-12.16 -12.59,-13.72 -8.53,-2.29 -19.06,0.64 -23.75,18.16l-0.47,-0.13C62.04,74.12 72.21,63.88 83.94,60.78c2.48,-0.65 5.05,-1 7.66,-0.97 9.07,0.13 18.44,4.88 24.56,17.13 0.09,0.17 0.17,0.35 0.25,0.53 5.21,15.23 2.11,43.32 -3.34,57.63 -7.29,18.75 -22.38,40.5 -47.69,65.5C6.99,258.25 4,329.82 39.97,388.81 75.94,447.8 152.13,493.56 254.44,493.56c102.31,0 178.47,-45.76 214.44,-104.75 35.88,-58.85 32.98,-130.23 -25,-187.81l-0.41,-0.41h-0.03c-25.31,-25 -40.37,-46.75 -47.66,-65.5 -5.23,-16.45 -9.09,-42.99 -2.65,-57.63 0.06,-0.13 0.13,-0.25 0.19,-0.38 0.03,-0.05 0.04,-0.11 0.06,-0.16 6.12,-12.25 15.49,-17 24.56,-17.13 2.6,-0.04 5.18,0.31 7.66,0.97 11.72,3.1 21.87,13.34 19.34,32.84l-0.44,0.13c-4.69,-17.52 -15.22,-20.45 -23.75,-18.16 -4.41,1.18 -8.6,3.85 -10.97,8.56 -0.01,0.04 -0.02,0.09 -0.03,0.13 -0.98,3.01 -1.5,6.2 -1.5,9.53 0,17.09 13.85,30.94 30.94,30.94 17.09,0 30.94,-13.85 30.94,-30.94 0,-4.36 -0.91,-8.49 -2.53,-12.25 -3.06,-13.24 -10.6,-23.11 -20.44,-30.69 -14.46,-11.13 -34.39,-16.36 -49.94,-15.78 -13.38,0.5 -24.85,4.11 -33.22,10.53 -3.41,2.62 -6.38,5.7 -8.84,9.38 -69.46,35.51 -138.89,38.75 -208.34,-7.75 -0.64,-0.56 -1.29,-1.11 -1.97,-1.63 -8.37,-6.42 -19.84,-10.03 -33.22,-10.53 -0.97,-0.04 -1.96,-0.04 -2.97,-0.03zM161.44,88.19c6.34,2.65 12.67,4.99 19,7.03v313.06c-30.73,-8.26 -57.89,-22 -77.37,-41.31 -17.1,-16.94 -28.08,-38.63 -28.91,-63.6 -0.83,-24.97 8.27,-52.7 28.97,-82.63 41.32,-59.75 57.16,-103.6 58.31,-132.56zM347.47,89.59c1.6,28.97 17.59,72.37 58.25,131.16 20.69,29.92 29.8,57.66 28.97,82.63 -0.83,24.97 -11.81,46.65 -28.91,63.59 -19.02,18.85 -45.37,32.4 -75.22,40.72L330.56,94.84c5.64,-1.61 11.27,-3.35 16.91,-5.25zM311.87,99.59v312.53c-8.21,1.63 -16.61,2.88 -25.13,3.78L286.75,104.09c8.38,-1.12 16.75,-2.63 25.13,-4.5zM199.12,100.37c8.45,1.97 16.89,3.44 25.34,4.44v311.34c-8.59,-0.84 -17.06,-2.05 -25.34,-3.63L199.12,100.38zM268.06,105.94v311.38c-4.54,0.2 -9.09,0.31 -13.66,0.31 -3.76,0 -7.51,-0.05 -11.25,-0.19L243.16,106.28c8.29,0.31 16.61,0.18 24.91,-0.34z" />
</vector>

View file

@ -116,8 +116,10 @@
<string name="character_sheet_proficiency_stealth">Discrétion</string>
<string name="character_sheet_proficiency_survival">Survie</string>
<string name="character_sheet_action_spell_cast">Lancer</string>
<string name="character_sheet_action_spell_level_0">Cantrip</string>
<string name="character_sheet_action_spell_level_X">Sort de niveau %1$s</string>
<string name="character_sheet_action_spell_level_0">Sorts mineurs</string>
<string name="character_sheet_action_spell_level_X">Sorts de niveau %1$s</string>
<string name="character_sheet_skill_title">Capacités</string>
<string name="character_sheet_attack_title">Attaques</string>
<string name="dice_roll_mastery_proficiency">Maîtrise \"%1$s\" </string>
<string name="dice_roll_mastery_expertise">Expertise \"%1$s\" </string>
@ -156,10 +158,4 @@
<string name="alteration_target">Cible : %1$s</string>
<string name="no_available_description">Aucune description disponnible</string>
<string name="token_label_title">Capacité</string>
<string name="token_label_rage">Rage</string>
<string name="token_label_relentless_endurance">Endurance Implacable</string>
<string name="token_label_bardic_inspiration">Inspiration Bardique</string>
<string name="token_label_divine_conduit">Conduit Divin</string>
</resources>

View file

@ -118,6 +118,8 @@
<string name="character_sheet_action_spell_cast">Cast</string>
<string name="character_sheet_action_spell_level_0">Cantrip</string>
<string name="character_sheet_action_spell_level_X">Spell level %1$s</string>
<string name="character_sheet_skill_title">Skills</string>
<string name="character_sheet_attack_title">Attacks</string>
<string name="dice_roll_mastery_proficiency">%1$s proficiency</string>
<string name="dice_roll_mastery_expertise">%1$s expertise</string>
@ -156,10 +158,4 @@
<string name="alteration_target">Target: %1$s</string>
<string name="no_available_description">No available description</string>
<string name="token_label_title">Skill</string>
<string name="token_label_rage">Rage</string>
<string name="token_label_relentless_endurance">Relentless Endurance</string>
<string name="token_label_bardic_inspiration">Bardic Inspiration</string>
<string name="token_label_divine_conduit">Divine Conduit</string>
</resources>