DnDApplication/app/src/main/java/com/pixelized/rplexicon/business/DiceThrowUseCase.kt

1085 lines
No EOL
49 KiB
Kotlin

package com.pixelized.rplexicon.business
import android.app.Application
import android.content.Context
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.data.model.Alteration
import com.pixelized.rplexicon.data.model.AssignedSpell
import com.pixelized.rplexicon.data.model.CharacterSheet
import com.pixelized.rplexicon.data.model.DiceThrow
import com.pixelized.rplexicon.data.model.Property
import com.pixelized.rplexicon.data.model.Skill
import com.pixelized.rplexicon.data.model.Throw
import com.pixelized.rplexicon.data.repository.character.ActionRepository
import com.pixelized.rplexicon.data.repository.character.AlterationRepository
import com.pixelized.rplexicon.data.repository.character.CharacterSheetRepository
import com.pixelized.rplexicon.data.repository.character.ObjectActionRepository
import com.pixelized.rplexicon.data.repository.character.SkillRepository
import com.pixelized.rplexicon.data.repository.character.SpellRepository
import com.pixelized.rplexicon.ui.screens.rolls.composable.RollDiceUio
import com.pixelized.rplexicon.ui.screens.rolls.composable.ThrowsCardUio
import com.pixelized.rplexicon.utilitary.extentions.icon
import com.pixelized.rplexicon.utilitary.extentions.local.advantage
import com.pixelized.rplexicon.utilitary.extentions.local.critical
import com.pixelized.rplexicon.utilitary.extentions.local.disadvantage
import com.pixelized.rplexicon.utilitary.extentions.local.fail
import com.pixelized.rplexicon.utilitary.extentions.local.isBrutalCritical
import com.pixelized.rplexicon.utilitary.extentions.local.isCritical
import com.pixelized.rplexicon.utilitary.extentions.local.isSavageAttacks
import com.pixelized.rplexicon.utilitary.extentions.local.sum
import com.pixelized.rplexicon.utilitary.extentions.local.toStatus
import com.pixelized.rplexicon.utilitary.extentions.modifier
import com.pixelized.rplexicon.utilitary.extentions.toLabel
import javax.inject.Inject
import kotlin.math.max
import kotlin.math.min
data class DiceThrowResult(
val dice: RollDiceUio,
val card: ThrowsCardUio,
)
@Suppress("KotlinConstantConditions")
class DiceThrowUseCase @Inject constructor(
private val application: Application,
private val characterSheetRepository: CharacterSheetRepository,
private val actionRepository: ActionRepository,
private val objectRepository: ObjectActionRepository,
private val spellRepository: SpellRepository,
private val skillRepository: SkillRepository,
private val alterationRepository: AlterationRepository,
) {
fun roll(diceThrow: DiceThrow, alterationId: List<String>): DiceThrowResult? {
val sheet = characterSheetRepository.find(name = diceThrow.character)
val alterations = alterationRepository.getAlterations(character = diceThrow.character)
.filter { alterationId.contains(it.name) }
return if (sheet != null) {
when (diceThrow) {
is DiceThrow.Initiative -> abilityThrow(
character = sheet,
alterations = alterations,
abilityLabel = { getString(R.string.character_sheet_title_initiative) },
relatedLabel = { getString(R.string.character_sheet_stat_dexterity) },
ability = Property.INITIATIVE,
relatedStat = Property.DEXTERITY,
)
is DiceThrow.Strength -> abilityThrow(
character = sheet,
alterations = alterations,
abilityLabel = { getString(R.string.character_sheet_stat_strength) },
relatedLabel = { getString(R.string.character_sheet_stat_strength) },
ability = Property.STRENGTH_THROW,
relatedStat = Property.STRENGTH,
)
is DiceThrow.Dexterity -> abilityThrow(
character = sheet,
alterations = alterations,
abilityLabel = { getString(R.string.character_sheet_stat_dexterity) },
relatedLabel = { getString(R.string.character_sheet_stat_dexterity) },
ability = Property.DEXTERITY_THROW,
relatedStat = Property.DEXTERITY,
)
is DiceThrow.Constitution -> abilityThrow(
character = sheet,
alterations = alterations,
abilityLabel = { getString(R.string.character_sheet_stat_constitution) },
relatedLabel = { getString(R.string.character_sheet_stat_constitution) },
ability = Property.CONSTITUTION_THROW,
relatedStat = Property.CONSTITUTION,
)
is DiceThrow.Intelligence -> abilityThrow(
character = sheet,
alterations = alterations,
abilityLabel = { getString(R.string.character_sheet_stat_intelligence) },
relatedLabel = { getString(R.string.character_sheet_stat_intelligence) },
ability = Property.INTELLIGENCE_THROW,
relatedStat = Property.INTELLIGENCE,
)
is DiceThrow.Wisdom -> abilityThrow(
character = sheet,
alterations = alterations,
abilityLabel = { getString(R.string.character_sheet_stat_wisdom) },
relatedLabel = { getString(R.string.character_sheet_stat_wisdom) },
ability = Property.WISDOM_THROW,
relatedStat = Property.WISDOM
)
is DiceThrow.Charisma -> abilityThrow(
character = sheet,
alterations = alterations,
abilityLabel = { getString(R.string.character_sheet_stat_charisma) },
relatedLabel = { getString(R.string.character_sheet_stat_charisma) },
ability = Property.CHARISMA_THROW,
relatedStat = Property.CHARISMA
)
is DiceThrow.StrengthSavingThrow -> savingThrow(
character = sheet,
alterations = alterations,
abilityLabel = { getString(R.string.character_sheet_stat_strength) },
relatedLabel = { getString(R.string.character_sheet_stat_strength) },
ability = Property.STRENGTH_SAVING_THROW,
relatedStat = Property.STRENGTH,
)
is DiceThrow.DexteritySavingThrow -> savingThrow(
character = sheet,
alterations = alterations,
abilityLabel = { getString(R.string.character_sheet_stat_dexterity) },
relatedLabel = { getString(R.string.character_sheet_stat_dexterity) },
ability = Property.DEXTERITY_SAVING_THROW,
relatedStat = Property.DEXTERITY,
)
is DiceThrow.ConstitutionSavingThrow -> savingThrow(
character = sheet,
alterations = alterations,
abilityLabel = { getString(R.string.character_sheet_stat_constitution) },
relatedLabel = { getString(R.string.character_sheet_stat_constitution) },
ability = Property.CONSTITUTION_SAVING_THROW,
relatedStat = Property.CONSTITUTION,
)
is DiceThrow.IntelligenceSavingThrow -> savingThrow(
character = sheet,
alterations = alterations,
abilityLabel = { getString(R.string.character_sheet_stat_intelligence) },
relatedLabel = { getString(R.string.character_sheet_stat_intelligence) },
ability = Property.INTELLIGENCE_SAVING_THROW,
relatedStat = Property.INTELLIGENCE,
)
is DiceThrow.WisdomSavingThrow -> savingThrow(
character = sheet,
alterations = alterations,
abilityLabel = { getString(R.string.character_sheet_stat_wisdom) },
relatedLabel = { getString(R.string.character_sheet_stat_wisdom) },
ability = Property.WISDOM_SAVING_THROW,
relatedStat = Property.WISDOM
)
is DiceThrow.CharismaSavingThrow -> savingThrow(
character = sheet,
alterations = alterations,
abilityLabel = { getString(R.string.character_sheet_stat_charisma) },
relatedLabel = { getString(R.string.character_sheet_stat_charisma) },
ability = Property.CHARISMA_SAVING_THROW,
relatedStat = Property.CHARISMA
)
is DiceThrow.DeathSavingThrow -> savingThrow(
character = sheet,
alterations = alterations,
abilityLabel = { getString(R.string.character_sheet_stat_death) },
relatedLabel = { getString(R.string.character_sheet_stat_death) },
ability = Property.DEATH_SAVING_THROW,
relatedStat = null,
)
is DiceThrow.Acrobatics -> abilityThrow(
character = sheet,
alterations = alterations,
abilityLabel = { getString(R.string.character_sheet_proficiency_acrobatics) },
relatedLabel = { getString(R.string.character_sheet_stat_dexterity) },
ability = Property.ACROBATICS,
relatedStat = Property.DEXTERITY,
)
is DiceThrow.AnimalHandling -> abilityThrow(
character = sheet,
alterations = alterations,
abilityLabel = { getString(R.string.character_sheet_proficiency_animal_handling) },
relatedLabel = { getString(R.string.character_sheet_stat_wisdom) },
ability = Property.ANIMAL_HANDLING,
relatedStat = Property.WISDOM,
)
is DiceThrow.Arcana -> abilityThrow(
character = sheet,
alterations = alterations,
abilityLabel = { getString(R.string.character_sheet_proficiency_arcana) },
relatedLabel = { getString(R.string.character_sheet_stat_intelligence) },
ability = Property.ARCANA,
relatedStat = Property.INTELLIGENCE,
)
is DiceThrow.Athletics -> abilityThrow(
character = sheet,
alterations = alterations,
abilityLabel = { getString(R.string.character_sheet_proficiency_athletics) },
relatedLabel = { getString(R.string.character_sheet_stat_strength) },
ability = Property.ATHLETICS,
relatedStat = Property.STRENGTH,
)
is DiceThrow.Deception -> abilityThrow(
character = sheet,
alterations = alterations,
abilityLabel = { getString(R.string.character_sheet_proficiency_deception) },
relatedLabel = { getString(R.string.character_sheet_stat_charisma) },
ability = Property.DECEPTION,
relatedStat = Property.CHARISMA,
)
is DiceThrow.History -> abilityThrow(
character = sheet,
alterations = alterations,
abilityLabel = { getString(R.string.character_sheet_proficiency_history) },
relatedLabel = { getString(R.string.character_sheet_stat_intelligence) },
ability = Property.HISTORY,
relatedStat = Property.INTELLIGENCE,
)
is DiceThrow.Insight -> abilityThrow(
character = sheet,
alterations = alterations,
abilityLabel = { getString(R.string.character_sheet_proficiency_insight) },
relatedLabel = { getString(R.string.character_sheet_stat_wisdom) },
ability = Property.INSIGHT,
relatedStat = Property.WISDOM,
)
is DiceThrow.Intimidation -> abilityThrow(
character = sheet,
alterations = alterations,
abilityLabel = { getString(R.string.character_sheet_proficiency_intimidation) },
relatedLabel = { getString(R.string.character_sheet_stat_charisma) },
ability = Property.INTIMIDATION,
relatedStat = Property.CHARISMA,
)
is DiceThrow.Investigation -> abilityThrow(
character = sheet,
alterations = alterations,
abilityLabel = { getString(R.string.character_sheet_proficiency_investigation) },
relatedLabel = { getString(R.string.character_sheet_stat_intelligence) },
ability = Property.INVESTIGATION,
relatedStat = Property.INTELLIGENCE,
)
is DiceThrow.Medicine -> abilityThrow(
character = sheet,
alterations = alterations,
abilityLabel = { getString(R.string.character_sheet_proficiency_medicine) },
relatedLabel = { getString(R.string.character_sheet_stat_wisdom) },
ability = Property.MEDICINE,
relatedStat = Property.WISDOM,
)
is DiceThrow.Nature -> abilityThrow(
character = sheet,
alterations = alterations,
abilityLabel = { getString(R.string.character_sheet_proficiency_nature) },
relatedLabel = { getString(R.string.character_sheet_stat_intelligence) },
ability = Property.NATURE,
relatedStat = Property.INTELLIGENCE,
)
is DiceThrow.Perception -> abilityThrow(
character = sheet,
alterations = alterations,
abilityLabel = { getString(R.string.character_sheet_proficiency_perception) },
relatedLabel = { getString(R.string.character_sheet_stat_wisdom) },
ability = Property.PERCEPTION,
relatedStat = Property.WISDOM,
)
is DiceThrow.Performance -> abilityThrow(
character = sheet,
alterations = alterations,
abilityLabel = { getString(R.string.character_sheet_proficiency_performance) },
relatedLabel = { getString(R.string.character_sheet_stat_charisma) },
ability = Property.PERFORMANCE,
relatedStat = Property.CHARISMA,
)
is DiceThrow.Persuasion -> abilityThrow(
character = sheet,
alterations = alterations,
abilityLabel = { getString(R.string.character_sheet_proficiency_persuasion) },
relatedLabel = { getString(R.string.character_sheet_stat_charisma) },
ability = Property.PERSUASION,
relatedStat = Property.CHARISMA,
)
is DiceThrow.Religion -> abilityThrow(
character = sheet,
alterations = alterations,
abilityLabel = { getString(R.string.character_sheet_proficiency_religion) },
relatedLabel = { getString(R.string.character_sheet_stat_intelligence) },
ability = Property.RELIGION,
relatedStat = Property.INTELLIGENCE,
)
is DiceThrow.SleightOfHand -> abilityThrow(
character = sheet,
alterations = alterations,
abilityLabel = { getString(R.string.character_sheet_proficiency_sleight_of_hand) },
relatedLabel = { getString(R.string.character_sheet_stat_dexterity) },
ability = Property.SLEIGHT_OF_HAND,
relatedStat = Property.DEXTERITY,
)
is DiceThrow.Stealth -> abilityThrow(
character = sheet,
alterations = alterations,
abilityLabel = { getString(R.string.character_sheet_proficiency_stealth) },
relatedLabel = { getString(R.string.character_sheet_stat_dexterity) },
ability = Property.STEALTH,
relatedStat = Property.DEXTERITY,
)
is DiceThrow.Survival -> abilityThrow(
character = sheet,
alterations = alterations,
abilityLabel = { getString(R.string.character_sheet_proficiency_survival) },
relatedLabel = { getString(R.string.character_sheet_stat_wisdom) },
ability = Property.SURVIVAL,
relatedStat = Property.WISDOM,
)
is DiceThrow.PhysicalMeleeAttack -> {
val action = actionRepository.find(
character = diceThrow.character,
action = diceThrow.weapon,
)
val objectAlterations = alterationRepository.getAlterations(
objects = action?.alterations,
)
actionThrow(
character = sheet,
action = diceThrow.weapon,
diceThrow = action?.hit,
title = { getString(R.string.dice_roll_attack_hit_title, it) },
alterations = alterations + objectAlterations,
ability = Property.PHYSICAL_MELEE_ATTACK,
)
}
is DiceThrow.PhysicalMeleeDamage -> {
val action = actionRepository.find(
character = diceThrow.character,
action = diceThrow.weapon,
)
val objectAlterations = alterationRepository.getAlterations(
objects = action?.alterations,
)
actionThrow(
character = sheet,
action = diceThrow.weapon,
diceThrow = action?.damage,
title = { getString(R.string.dice_roll_attack_damage_title, it) },
alterations = alterations + objectAlterations,
ability = Property.PHYSICAL_MELEE_DAMAGE,
)
}
is DiceThrow.PhysicalRangeAttack -> {
val action = actionRepository.find(
character = diceThrow.character,
action = diceThrow.weapon,
)
val objectAlterations = alterationRepository.getAlterations(
objects = action?.alterations,
)
actionThrow(
character = sheet,
action = diceThrow.weapon,
diceThrow = action?.hit,
title = { getString(R.string.dice_roll_attack_hit_title, it) },
alterations = alterations + objectAlterations,
ability = Property.PHYSICAL_RANGE_ATTACK,
)
}
is DiceThrow.PhysicalRangeDamage -> {
val action = actionRepository.find(
character = diceThrow.character,
action = diceThrow.weapon,
)
val objectAlterations = alterationRepository.getAlterations(
objects = action?.alterations,
)
actionThrow(
character = sheet,
action = diceThrow.weapon,
diceThrow = action?.damage,
title = { getString(R.string.dice_roll_attack_damage_title, it) },
alterations = alterations + objectAlterations,
ability = Property.PHYSICAL_RANGE_DAMAGE,
)
}
is DiceThrow.Object -> {
val action = objectRepository.find(
character = diceThrow.character,
item = diceThrow.item,
)
actionThrow(
character = sheet,
action = diceThrow.item,
diceThrow = action?.effect,
title = { getString(R.string.dice_roll_use_object, it) },
alterations = alterations,
ability = Property.OBJECT_EFFECT,
)
}
is DiceThrow.SpellAttack -> {
val spell = spellRepository.findAssignedSpell(
character = diceThrow.character,
spell = diceThrow.spell,
)
actionThrow(
character = sheet,
action = diceThrow.spell,
diceThrow = spell?.hit,
title = { getString(R.string.dice_roll_spell_hit_title, it) },
alterations = alterations,
ability = Property.SPELL_ATTACK,
)
}
is DiceThrow.SpellDamage -> {
val spell = spellRepository.findAssignedSpell(
character = diceThrow.character,
spell = diceThrow.spell,
)
actionThrow(
character = sheet,
action = diceThrow.spell,
diceThrow = spell?.effect,
title = { getString(R.string.dice_roll_spell_damage_title, it) },
alterations = alterations,
ability = Property.SPELL_DAMAGE,
)
}
is DiceThrow.SpellEffect -> {
val spell = spellRepository.findAssignedSpell(
character = diceThrow.character,
spell = diceThrow.spell,
)
spellThrow(
character = sheet,
spell = spell,
level = diceThrow.level,
alterations = alterations,
)
}
is DiceThrow.Skill -> {
val skill = skillRepository.find(
character = diceThrow.character,
skill = diceThrow.skill,
)
skillThrow(
character = sheet,
alterations = alterations,
skill = skill,
)
}
}
} else {
null
}
}
private fun abilityThrow(
character: CharacterSheet,
alterations: List<Alteration>,
abilityLabel: Context.() -> String,
relatedLabel: Context.() -> String?,
ability: Property,
relatedStat: Property,
): DiceThrowResult = rollAbility(
character = character,
alterations = alterations,
abilityTitle = { getString(R.string.dice_roll_check_title, it) },
abilityLabel = abilityLabel,
relatedTitle = { label -> getString(R.string.dice_roll_bonus_detail, label) },
relatedLabel = relatedLabel,
ability = ability,
relatedStat = relatedStat,
)
private fun savingThrow(
character: CharacterSheet,
alterations: List<Alteration>,
abilityLabel: Context.() -> String,
relatedLabel: Context.() -> String?,
ability: Property,
relatedStat: Property?,
): DiceThrowResult = rollAbility(
character = character,
alterations = alterations,
abilityTitle = { getString(R.string.dice_roll_saving_throw_title, it) },
abilityLabel = abilityLabel,
relatedTitle = { getString(R.string.dice_roll_saving_throw_detail, it) },
relatedLabel = relatedLabel,
ability = ability,
relatedStat = relatedStat,
)
private fun rollAbility(
character: CharacterSheet,
alterations: List<Alteration>,
abilityTitle: Context.(label: String) -> String,
abilityLabel: Context.() -> String,
relatedTitle: Context.(label: String) -> String?,
relatedLabel: Context.() -> String?,
ability: Property,
relatedStat: Property?,
): DiceThrowResult {
with(ThrowScope(context = application, character = character, alterations = alterations)) {
// retrieve some wording.
val abilityLabelString = abilityLabel(application)
val abilityTitleString = abilityTitle(application, abilityLabelString)
// check if the roll is affected by some status.
val advantage = status[ability].advantage
val disadvantage = status[ability].disadvantage
// main roll
val result = roll(
advantage = advantage,
disadvantage = disadvantage,
fail = status[ability].fail,
critical = status[ability].critical,
)
// fetch and build a list of dice roll base on alterations.
val diceAlterationBonus = status[ability].alterationsBonus()
// fetch and build a list of flat bonus
val flatAlterationBonus = status[ability].flatAlterationBonus()
// fetch and build the associated characteristic
val relatedStatBonus = when (relatedStat) {
Property.STRENGTH -> (character.strength + status[relatedStat].sum).modifier
Property.DEXTERITY -> (character.dexterity + status[relatedStat].sum).modifier
Property.CONSTITUTION -> (character.constitution + status[relatedStat].sum).modifier
Property.INTELLIGENCE -> (character.intelligence + status[relatedStat].sum).modifier
Property.WISDOM -> (character.wisdom + status[relatedStat].sum).modifier
Property.CHARISMA -> (character.charisma + status[relatedStat].sum).modifier
Property.PROFICIENCY -> character.proficiency
else -> null
}?.let { value ->
val relatedLabelString = relatedLabel(application)
val relatedTitleString = relatedLabelString?.let { relatedTitle(application, it) }
if (relatedTitleString != null) {
allValue.add(value)
listOf(
ThrowsCardUio.Detail(
title = relatedTitleString,
result = "$value",
)
)
} else {
null
}
} ?: emptyList()
// fetch and build the mastery bonus
val mastery = when (ability) {
Property.STRENGTH_SAVING_THROW -> character.strengthSavingThrows
Property.DEXTERITY_SAVING_THROW -> character.dexteritySavingThrows
Property.CONSTITUTION_SAVING_THROW -> character.constitutionSavingThrows
Property.INTELLIGENCE_SAVING_THROW -> character.intelligenceSavingThrows
Property.WISDOM_SAVING_THROW -> character.wisdomSavingThrows
Property.CHARISMA_SAVING_THROW -> character.charismaSavingThrows
Property.ACROBATICS -> character.acrobatics
Property.ANIMAL_HANDLING -> character.animalHandling
Property.ARCANA -> character.arcana
Property.ATHLETICS -> character.athletics
Property.DECEPTION -> character.deception
Property.HISTORY -> character.history
Property.INSIGHT -> character.insight
Property.INTIMIDATION -> character.intimidation
Property.INVESTIGATION -> character.investigation
Property.MEDICINE -> character.medicine
Property.NATURE -> character.nature
Property.PERCEPTION -> character.perception
Property.PERFORMANCE -> character.performance
Property.PERSUASION -> character.persuasion
Property.RELIGION -> character.religion
Property.SLEIGHT_OF_HAND -> character.sleightOfHand
Property.STEALTH -> character.stealth
Property.SURVIVAL -> character.survival
else -> null
}?.let { multiplier ->
val mastery = character.proficiency * multiplier
allValue.add(mastery)
listOf(
ThrowsCardUio.Detail(
title = application.getString(
when (multiplier) {
2 -> R.string.dice_roll_expertise_bonus
else -> R.string.dice_roll_proficiency_bonus
}
),
result = "$mastery",
),
)
} ?: emptyList()
// compute the final roll result
val rollResult = when (result.value) {
1, 20 -> "${result.value}"
else -> "${allValue.sum()}"
}
// build the result.
return DiceThrowResult(
dice = RollDiceUio(
isCriticalSuccess = result.value == 20,
isCriticalFailure = result.value == 1,
result = rollResult,
),
card = ThrowsCardUio(
title = abilityTitleString.uppercase(),
highlight = abilityLabelString.uppercase(),
dice = R.drawable.ic_d20_24,
roll = allValue.toLabel(),
result = rollResult,
isCriticalSuccess = result.value == 20,
isCriticalFailure = result.value == 1,
details = listOf(
ThrowsCardUio.Detail(
title = abilityTitleString,
throws = ThrowsCardUio.Throw(
dice = R.drawable.ic_d20_24,
advantage = advantage,
disadvantage = disadvantage,
roll = "1d20",
result = result.label,
),
result = "${result.value}",
),
) + diceAlterationBonus + flatAlterationBonus + relatedStatBonus + mastery,
)
)
}
}
private fun actionThrow(
character: CharacterSheet,
action: String,
diceThrow: Throw?,
title: Context.(action: String) -> String,
alterations: List<Alteration>,
ability: Property?,
): DiceThrowResult {
with(ThrowScope(context = application, character = character, alterations = alterations)) {
// retrieve some wording.
val titleString = title(application, action)
// check if this throw can be a critical success of failure.
val canMakeCriticalRoll = when (ability) {
Property.PHYSICAL_MELEE_ATTACK -> true
Property.PHYSICAL_RANGE_ATTACK -> true
Property.SPELL_ATTACK -> true
else -> false
}
// check if the roll is affected by some status.
val advantage = status[ability].advantage
val disadvantage = status[ability].disadvantage
// compute the amount of main dice to throw.
val amount = if (status.isCritical) {
diceThrow?.amount?.times(2)?.let {
if (ability == Property.PHYSICAL_MELEE_DAMAGE && status.isSavageAttacks)
it.plus(1) else it
}?.let {
if (ability == Property.PHYSICAL_MELEE_DAMAGE && status.isBrutalCritical)
it.plus(1) else it
}
} else {
diceThrow?.amount
}
// main roll
val result = roll(
amount = amount ?: 1,
faces = diceThrow?.faces ?: 20,
advantage = advantage,
disadvantage = disadvantage,
fail = status[ability].fail,
critical = status[ability].critical,
)
// fetch and build a list of dice roll base on alterations.
val diceAlterationBonus = status[ability].alterationsBonus()
// fetch and build a list of flat bonus
val flatAlterationBonus = status[ability].flatAlterationBonus()
// fetch and build the associated characteristic
val relatedStatBonus = diceThrow.statBonus(name = action)
// check for flat dice bonus (ex: healing potion 2d4 + 2)
val flatBonus = diceThrow?.flat?.takeIf { it > 0 }?.let {
allValue.add(it)
listOf(
ThrowsCardUio.Detail(
title = application.getString(R.string.dice_roll_bonus_detail, action),
result = "$it",
)
)
} ?: emptyList()
// compute the final roll result
val rollResult = when {
canMakeCriticalRoll && result.value == 20 -> "20"
canMakeCriticalRoll && result.value == 1 -> "1"
else -> "${allValue.sum()}"
}
// build the result.
return DiceThrowResult(
dice = RollDiceUio(
icon = diceThrow?.faces?.icon ?: R.drawable.ic_d20_24,
isCriticalSuccess = canMakeCriticalRoll && result.value == 20,
isCriticalFailure = canMakeCriticalRoll && result.value == 1,
result = rollResult,
),
card = ThrowsCardUio(
title = titleString.uppercase(),
highlight = action,
dice = diceThrow?.faces?.icon ?: R.drawable.ic_d20_24,
isCriticalSuccess = canMakeCriticalRoll && result.value == 20,
isCriticalFailure = canMakeCriticalRoll && result.value == 1,
roll = allValue.toLabel(),
result = rollResult,
details = listOf(
ThrowsCardUio.Detail(
title = action,
throws = ThrowsCardUio.Throw(
dice = diceThrow?.faces?.icon ?: R.drawable.ic_d20_24,
advantage = advantage,
disadvantage = disadvantage,
roll = "${diceThrow?.amount}d${diceThrow?.faces}",
result = result.label,
),
result = "${result.value}",
),
) + diceAlterationBonus + flatAlterationBonus + relatedStatBonus + flatBonus,
)
)
}
}
private fun spellThrow(
character: CharacterSheet,
spell: AssignedSpell?,
level: Int,
alterations: List<Alteration>,
): DiceThrowResult {
with(ThrowScope(context = application, character = character, alterations = alterations)) {
// retrieve some wording.
val spellName = spell?.spell?.name ?: ""
val titleString = application.getString(R.string.dice_roll_spell_cast, spellName)
val advantage = status[Property.SPELL_EFFECT].advantage
val disadvantage = status[Property.SPELL_EFFECT].disadvantage
// main roll
val result = roll(
amount = spell?.effect?.amount ?: 1,
faces = spell?.effect?.faces ?: 4,
advantage = advantage,
disadvantage = disadvantage,
fail = status[Property.SPELL_EFFECT].fail,
critical = status[Property.SPELL_EFFECT].critical,
)
// fetch and build a list of additionnal level effect.
val levelBonus = if (spell?.level != null) {
((spell.spell.level + 1)..level).map {
val localRoll = roll(
amount = spell.level.amount,
faces = spell.level.faces,
advantage = advantage,
disadvantage = disadvantage,
fail = status[Property.SPELL_EFFECT].fail,
critical = status[Property.SPELL_EFFECT].critical,
)
ThrowsCardUio.Detail(
title = application.getString(R.string.spell_level_chooser_label, "$it"),
throws = ThrowsCardUio.Throw(
dice = spell.level.faces.icon,
roll = "${spell.level.amount}d${spell.level.faces}",
advantage = advantage,
disadvantage = disadvantage,
result = localRoll.label,
),
result = "${localRoll.value}",
)
}
} else {
emptyList()
}
// fetch and build the associated characteristic
val relatedStatBonus = spell?.effect.statBonus(name = spellName)
// compute the final roll result
val rollResult = "${allValue.sum()}"
// build the result.
return DiceThrowResult(
dice = RollDiceUio(
icon = (spell?.effect?.faces ?: 4).icon,
result = rollResult,
),
card = ThrowsCardUio(
title = titleString.uppercase(),
highlight = spellName,
dice = (spell?.effect?.faces ?: 4).icon,
roll = allValue.toLabel(),
result = rollResult,
details = listOf(
ThrowsCardUio.Detail(
title = spellName,
throws = ThrowsCardUio.Throw(
dice = (spell?.effect?.faces ?: 4).icon,
roll = "${spell?.effect?.amount ?: 1}d${spell?.effect?.faces ?: 4}",
advantage = advantage,
disadvantage = disadvantage,
result = result.label,
),
result = "${result.value}",
),
) + levelBonus + relatedStatBonus,
)
)
}
}
private fun skillThrow(
character: CharacterSheet,
alterations: List<Alteration>,
skill: Skill?,
): DiceThrowResult {
with(ThrowScope(context = application, character = character, alterations = alterations)) {
// retrieve some wording.
val spellName = skill?.name
val titleString = application.getString(R.string.dice_roll_spell_cast, spellName)
val advantage = status[Property.SKILL].advantage
val disadvantage = status[Property.SKILL].disadvantage
// main roll
val result = roll(
amount = skill?.effect?.amount ?: 1,
faces = skill?.effect?.faces ?: 4,
advantage = advantage,
disadvantage = disadvantage,
fail = status[Property.SKILL].fail,
critical = status[Property.SKILL].critical,
)
// fetch and build a list of dice roll base on alterations.
val diceAlterationBonus = status[Property.SKILL].alterationsBonus()
// fetch and build a list of flat bonus
val flatAlterationBonus = status[Property.SKILL].flatAlterationBonus()
// fetch and build the associated characteristic, proficiency or level
val relatedStatBonus = skill?.effect.statBonus(name = skill?.name)
// compute the final roll result
val rollResult = "${allValue.sum()}"
// build the result.
return DiceThrowResult(
dice = RollDiceUio(
icon = (skill?.effect?.faces ?: 4).icon,
result = rollResult,
),
card = ThrowsCardUio(
title = titleString.uppercase(),
highlight = spellName,
dice = (skill?.effect?.faces ?: 4).icon,
roll = allValue.toLabel(),
result = rollResult,
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}",
advantage = advantage,
disadvantage = disadvantage,
result = result.label,
),
result = "${result.value}",
),
) + diceAlterationBonus + flatAlterationBonus + relatedStatBonus,
)
)
}
}
/**
* Helper class to track rolls and declare helper method.
*/
private class ThrowScope(
val context: Context,
val allValue: MutableList<Int> = mutableListOf(),
val character: CharacterSheet,
val alterations: List<Alteration>,
) {
val status = this.alterations.toStatus()
/**
* Fetch any stats / proficiency / level related bonus and build a ThrowsCardUio.Detail for each.
*/
fun Throw?.statBonus(name: String?): List<ThrowsCardUio.Detail> {
return this?.modifier?.mapNotNull { it.statBonus(name = name) } ?: emptyList()
}
fun Property.statBonus(name: String?): ThrowsCardUio.Detail? {
return when (this) {
Property.STRENGTH -> (character.strength + this@ThrowScope.status[this].sum).modifier
Property.DEXTERITY -> (character.dexterity + this@ThrowScope.status[this].sum).modifier
Property.CONSTITUTION -> (character.constitution + this@ThrowScope.status[this].sum).modifier
Property.INTELLIGENCE -> (character.intelligence + this@ThrowScope.status[this].sum).modifier
Property.WISDOM -> (character.wisdom + this@ThrowScope.status[this].sum).modifier
Property.CHARISMA -> (character.charisma + this@ThrowScope.status[this].sum).modifier
Property.PROFICIENCY -> character.proficiency
Property.LEVEL -> character.level
else -> null
}?.let { value ->
val titleLabel = if (this == Property.PROFICIENCY) {
context.getString(R.string.dice_roll_mastery_proficiency, name)
} else {
val label = when (this) {
Property.STRENGTH -> context.getString(R.string.character_sheet_stat_strength)
Property.DEXTERITY -> context.getString(R.string.character_sheet_stat_dexterity)
Property.CONSTITUTION -> context.getString(R.string.character_sheet_stat_constitution)
Property.INTELLIGENCE -> context.getString(R.string.character_sheet_stat_intelligence)
Property.WISDOM -> context.getString(R.string.character_sheet_stat_wisdom)
Property.CHARISMA -> context.getString(R.string.character_sheet_stat_charisma)
Property.LEVEL -> context.getString(R.string.character_sheet_stat_level)
else -> ""
}
context.getString(R.string.dice_roll_bonus_detail, label)
}
allValue.add(value)
ThrowsCardUio.Detail(
title = titleLabel,
result = "$value",
)
}
}
/**
* Fetch any alteration related bonus and build a ThrowsCardUio.Detail for each.
*/
fun List<Alteration.Status>?.alterationsBonus(): List<ThrowsCardUio.Detail> {
return this?.flatMap { status ->
status.dices.map { dice ->
val localRoll = roll(
amount = if (this@ThrowScope.status.isCritical) dice.count * 2 else dice.count,
faces = dice.faces,
advantage = dice.advantage,
disadvantage = dice.disadvantage,
fail = status.fail,
critical = status.critical,
)
ThrowsCardUio.Detail(
title = dice.title,
throws = ThrowsCardUio.Throw(
dice = dice.faces.icon,
advantage = dice.advantage,
disadvantage = dice.disadvantage,
roll = "${dice.count}d${dice.faces}",
result = localRoll.label,
),
result = "${localRoll.value}",
)
}
} ?: emptyList()
}
/**
* Fetch any flat number related bonus and build a ThrowsCardUio.Detail for each.
*/
fun List<Alteration.Status>?.flatAlterationBonus(): List<ThrowsCardUio.Detail> {
return this?.flatMap { status ->
status.bonus.map { bonus ->
allValue.add(bonus.value)
ThrowsCardUio.Detail(
title = bonus.title,
result = "${bonus.value}",
)
}
} ?: emptyList()
}
/**
* Make a roll of dices.
*/
fun roll(
amount: Int = 1,
faces: Int = 20,
advantage: Boolean = false,
disadvantage: Boolean = false,
fail: Boolean = false,
critical: Boolean = false,
): DiceRollResult {
val result = when {
advantage && !disadvantage -> {
val roll = List(amount) {
val first = random(faces = faces, fail = fail, critical = critical)
val second = random(faces = faces, fail = fail, critical = critical)
first to second
}
DiceRollResult(
label = roll.joinToString(" + ") { "${it.first}~${it.second}" },
value = roll.sumOf { max(it.first, it.second) },
)
}
disadvantage && !advantage -> {
val roll = List(amount) {
val first = random(faces = faces, fail = fail, critical = critical)
val second = random(faces = faces, fail = fail, critical = critical)
first to second
}
DiceRollResult(
label = roll.joinToString(" + ") { "${it.first}~${it.second}" },
value = roll.sumOf { min(it.first, it.second) },
)
}
else -> {
val roll = List(amount) {
random(faces = faces, fail = fail, critical = critical)
}
DiceRollResult(
label = roll.toLabel(),
value = roll.sum(),
)
}
}
allValue.add(result.value)
return result
}
private fun random(faces: Int, fail: Boolean, critical: Boolean): Int = when {
fail && !critical -> 1
critical && !fail -> faces
else -> (Math.random() * faces + 1).toInt()
}
}
private data class DiceRollResult(
val label: String,
val value: Int,
)
}