DnDApplication/app/src/main/java/com/pixelized/rplexicon/business/DiceThrowUseCase.kt
Andres Gomez, Thomas (ITDV RL) 57aac23369 Add support for a second subclass.
2024-06-25 16:43:28 +02:00

1203 lines
No EOL
54 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.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.alteration.Alteration
import com.pixelized.rplexicon.data.model.roll.Flat
import com.pixelized.rplexicon.data.model.roll.Throw
import com.pixelized.rplexicon.data.network.NetworkThrow
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.utilitary.extentions.icon
import com.pixelized.rplexicon.utilitary.extentions.local.advantage
import com.pixelized.rplexicon.utilitary.extentions.local.base
import com.pixelized.rplexicon.utilitary.extentions.local.critical
import com.pixelized.rplexicon.utilitary.extentions.local.disadvantage
import com.pixelized.rplexicon.utilitary.extentions.local.emphasis
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.primary
import com.pixelized.rplexicon.utilitary.extentions.local.secondary
import com.pixelized.rplexicon.utilitary.extentions.local.sum
import com.pixelized.rplexicon.utilitary.extentions.local.tertiary
import com.pixelized.rplexicon.utilitary.extentions.local.toStatus
import com.pixelized.rplexicon.utilitary.extentions.masteryMultiplier
import com.pixelized.rplexicon.utilitary.extentions.modifier
import com.pixelized.rplexicon.utilitary.extentions.toLabel
import kotlinx.coroutines.flow.firstOrNull
import javax.inject.Inject
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
import kotlin.random.Random
data class DiceThrowResult(
val dice: RollDiceUio,
val throws: NetworkThrow,
)
@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,
) {
suspend fun roll(
diceThrow: DiceThrow,
isThrowHidden: Boolean,
alterationId: List<String>,
): DiceThrowResult? {
val sheet = characterSheetRepository.find(name = diceThrow.character)
val alterations =
alterationRepository.getAssignedAlterations(diceThrow.character).firstOrNull()
?.filter { alterationId.contains(it.name) } ?: emptyList()
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,
isThrowHidden = isThrowHidden,
)
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,
isThrowHidden = isThrowHidden,
)
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,
isThrowHidden = isThrowHidden,
)
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,
isThrowHidden = isThrowHidden,
)
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,
isThrowHidden = isThrowHidden,
)
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,
isThrowHidden = isThrowHidden,
)
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,
isThrowHidden = isThrowHidden,
)
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,
isThrowHidden = isThrowHidden,
)
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,
isThrowHidden = isThrowHidden,
)
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,
isThrowHidden = isThrowHidden,
)
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,
isThrowHidden = isThrowHidden,
)
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,
isThrowHidden = isThrowHidden,
)
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,
isThrowHidden = isThrowHidden,
)
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,
isThrowHidden = isThrowHidden,
)
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,
isThrowHidden = isThrowHidden,
)
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,
isThrowHidden = isThrowHidden,
)
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,
isThrowHidden = isThrowHidden,
)
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,
isThrowHidden = isThrowHidden,
)
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,
isThrowHidden = isThrowHidden,
)
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,
isThrowHidden = isThrowHidden,
)
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,
isThrowHidden = isThrowHidden,
)
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,
isThrowHidden = isThrowHidden,
)
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,
isThrowHidden = isThrowHidden,
)
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,
isThrowHidden = isThrowHidden,
)
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,
isThrowHidden = isThrowHidden,
)
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,
isThrowHidden = isThrowHidden,
)
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,
isThrowHidden = isThrowHidden,
)
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,
isThrowHidden = isThrowHidden,
)
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,
isThrowHidden = isThrowHidden,
)
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,
isThrowHidden = isThrowHidden,
)
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,
isThrowHidden = isThrowHidden,
)
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,
isThrowHidden = isThrowHidden,
)
is DiceThrow.PhysicalMeleeAttack -> {
val action = actionRepository.find(
character = diceThrow.character,
action = diceThrow.weapon,
)
actionThrow(
character = sheet,
action = diceThrow.weapon,
diceThrow = action?.hit,
title = { getString(R.string.dice_roll_attack_hit_title, it) },
alterations = alterations,
ability = Property.PHYSICAL_MELEE_ATTACK,
)
}
is DiceThrow.PhysicalMeleeDamage -> {
val action = actionRepository.find(
character = diceThrow.character,
action = diceThrow.weapon,
)
actionThrow(
character = sheet,
action = diceThrow.weapon,
diceThrow = action?.damage,
title = { getString(R.string.dice_roll_attack_damage_title, it) },
alterations = alterations,
ability = Property.PHYSICAL_MELEE_DAMAGE,
)
}
is DiceThrow.PhysicalRangeAttack -> {
val action = actionRepository.find(
character = diceThrow.character,
action = diceThrow.weapon,
)
actionThrow(
character = sheet,
action = diceThrow.weapon,
diceThrow = action?.hit,
title = { getString(R.string.dice_roll_attack_hit_title, it) },
alterations = alterations,
ability = Property.PHYSICAL_RANGE_ATTACK,
)
}
is DiceThrow.PhysicalRangeDamage -> {
val action = actionRepository.find(
character = diceThrow.character,
action = diceThrow.weapon,
)
actionThrow(
character = sheet,
action = diceThrow.weapon,
diceThrow = action?.damage,
title = { getString(R.string.dice_roll_attack_damage_title, it) },
alterations = alterations,
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,
isThrowHidden: Boolean,
): 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,
isThrowHidden = isThrowHidden,
)
private fun savingThrow(
character: CharacterSheet,
alterations: List<Alteration>,
abilityLabel: Context.() -> String,
relatedLabel: Context.() -> String?,
ability: Property,
relatedStat: Property?,
isThrowHidden: Boolean,
): 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,
isThrowHidden = isThrowHidden,
)
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?,
isThrowHidden: Boolean,
): 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
val emphasis = status[ability].emphasis
// main roll
val result = roll(
advantage = advantage,
disadvantage = disadvantage,
emphasis = emphasis,
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(
NetworkThrow.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 {
val multiplier = it.masteryMultiplier(status = status[ability])
val mastery = character.proficiency * multiplier
allValue.add(mastery)
listOf(
NetworkThrow.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,
),
throws = NetworkThrow(
timestamp = System.currentTimeMillis(),
title = abilityTitleString.uppercase(),
highlight = abilityLabelString.uppercase(),
hidden = isThrowHidden,
face = 20,
roll = allValue.toLabel(),
result = rollResult,
isCriticalSuccess = result.value == 20,
isCriticalFailure = result.value == 1,
details = listOf(
NetworkThrow.Detail(
title = abilityTitleString,
throws = NetworkThrow.Throw(
dice = R.drawable.ic_d20_24,
advantage = advantage,
disadvantage = disadvantage,
emphasis = emphasis,
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
val emphasis = status[ability].emphasis
// compute the amount of main dice to throw.
val amount = if (status.isCritical) {
diceThrow?.dice?.count?.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?.dice?.count
}
// main roll
val result = roll(
amount = amount ?: 1,
faces = diceThrow?.dice?.faces ?: 20,
advantage = advantage,
disadvantage = disadvantage,
emphasis = emphasis,
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 relatedFlatBonus = diceThrow.flatBonus(name = action)
// 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?.dice?.icon ?: R.drawable.ic_d20_24,
isCriticalSuccess = canMakeCriticalRoll && result.value == 20,
isCriticalFailure = canMakeCriticalRoll && result.value == 1,
result = rollResult,
),
throws = NetworkThrow(
timestamp = System.currentTimeMillis(),
title = titleString.uppercase(),
highlight = action.uppercase(),
face = diceThrow?.dice?.faces ?: 20,
isCriticalSuccess = canMakeCriticalRoll && result.value == 20,
isCriticalFailure = canMakeCriticalRoll && result.value == 1,
roll = allValue.toLabel(),
result = rollResult,
details = listOf(
NetworkThrow.Detail(
title = action,
throws = NetworkThrow.Throw(
dice = diceThrow?.dice?.icon ?: R.drawable.ic_d20_24,
advantage = advantage,
disadvantage = disadvantage,
emphasis = emphasis,
roll = "${diceThrow?.dice?.count}d${diceThrow?.dice?.faces}",
result = result.label,
),
result = "${result.value}",
),
) + diceAlterationBonus + flatAlterationBonus + relatedStatBonus + relatedFlatBonus,
),
)
}
}
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
val emphasis = status[Property.SPELL_EFFECT].emphasis
// main roll
val result = roll(
amount = spell?.effect?.dice?.count ?: 1,
faces = spell?.effect?.dice?.faces ?: 4,
advantage = advantage,
disadvantage = disadvantage,
emphasis = emphasis,
fail = status[Property.SPELL_EFFECT].fail,
critical = status[Property.SPELL_EFFECT].critical,
)
// fetch and build a list of additional level effect.
val levelBonus = if (spell?.level?.dice != null) {
((spell.spell.level + 1)..level).map {
val localRoll = roll(
amount = spell.level.dice.count,
faces = spell.level.dice.faces,
advantage = advantage,
disadvantage = disadvantage,
emphasis = emphasis,
fail = status[Property.SPELL_EFFECT].fail,
critical = status[Property.SPELL_EFFECT].critical,
)
NetworkThrow.Detail(
title = application.getString(R.string.spell_level_chooser_label, "$it"),
throws = NetworkThrow.Throw(
dice = spell.level.dice.icon,
roll = spell.level.dice.toLabel(),
advantage = advantage,
disadvantage = disadvantage,
emphasis = emphasis,
result = localRoll.label,
),
result = "${localRoll.value}",
)
}
} else {
emptyList()
}
// fetch and build the associated characteristic
val relatedStatBonus = spell?.effect.statBonus(name = spellName)
// fetch and build a list of flat bonus
val relatedFlatBonus = spell?.effect?.flatBonus(name = spellName) ?: emptyList()
// compute the final roll result
val rollResult = "${allValue.sum()}"
// build the result.
return DiceThrowResult(
dice = RollDiceUio(
icon = spell?.effect?.dice?.icon ?: R.drawable.ic_d4_24,
result = rollResult,
),
throws = NetworkThrow(
timestamp = System.currentTimeMillis(),
title = titleString.uppercase(),
highlight = spellName.uppercase(),
face = spell?.effect?.dice?.faces ?: 4,
roll = allValue.toLabel(),
result = rollResult,
details = listOf(
NetworkThrow.Detail(
title = spellName,
throws = NetworkThrow.Throw(
dice = spell?.effect?.dice?.icon ?: R.drawable.ic_d4_24,
roll = spell?.effect?.dice?.toLabel() ?: "1d4",
advantage = advantage,
disadvantage = disadvantage,
emphasis = emphasis,
result = result.label,
),
result = "${result.value}",
),
) + levelBonus + relatedStatBonus + relatedFlatBonus,
),
)
}
}
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
val emphasis = status[Property.SKILL].emphasis
// main roll
val result = roll(
amount = skill?.effect?.dice?.count ?: 1,
faces = skill?.effect?.dice?.faces ?: 4,
advantage = advantage,
disadvantage = disadvantage,
emphasis = emphasis,
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)
// fetch and build a list of flat bonus
val relatedFlatBonus = skill?.effect.flatBonus(name = skill?.name)
// compute the final roll result
val rollResult = "${allValue.sum()}"
// build the result.
return DiceThrowResult(
dice = RollDiceUio(
icon = skill?.effect?.dice?.icon ?: R.drawable.ic_d4_24,
result = rollResult,
),
throws = NetworkThrow(
timestamp = System.currentTimeMillis(),
title = titleString.uppercase(),
highlight = spellName?.uppercase() ?: "",
face = skill?.effect?.dice?.faces ?: 4,
roll = allValue.toLabel(),
result = rollResult,
details = listOf(
NetworkThrow.Detail(
title = spellName ?: "",
throws = NetworkThrow.Throw(
dice = skill?.effect?.dice?.icon ?: R.drawable.ic_d4_24,
roll = skill?.effect?.dice?.toLabel() ?: "1d4",
advantage = advantage,
disadvantage = disadvantage,
emphasis = emphasis,
result = result.label,
),
result = "${result.value}",
),
) + diceAlterationBonus + flatAlterationBonus + relatedStatBonus + relatedFlatBonus,
),
)
}
}
/**
* 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<NetworkThrow.Detail> {
return this?.modifier?.mapNotNull { it.statBonus(name = name) } ?: emptyList()
}
/**
* Fetch any flat bonus related to the throw.
*/
fun Throw?.flatBonus(name: String?): List<NetworkThrow.Detail> {
return this?.flat?.flatBonus(name)?.let { listOf(it) } ?: emptyList()
}
private fun Property.statBonus(name: String?): NetworkThrow.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.base
Property.LEVEL_PC -> character.level.primary
Property.LEVEL_SC -> character.level.secondary
Property.LEVEL_TC -> character.level.tertiary
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)
Property.LEVEL_PC -> context.getString(R.string.character_sheet_stat_level_pc)
Property.LEVEL_SC -> context.getString(R.string.character_sheet_stat_level_sc)
Property.LEVEL_TC -> context.getString(R.string.character_sheet_stat_level_tc)
else -> ""
}
context.getString(R.string.dice_roll_bonus_detail, label)
}
allValue.add(value)
NetworkThrow.Detail(
title = titleLabel,
result = "$value",
)
}
}
private fun Flat.flatBonus(name: String?): NetworkThrow.Detail? {
return when {
value != 0 -> {
allValue.add(value)
NetworkThrow.Detail(
title = context.getString(R.string.dice_roll_bonus_detail, name),
result = "$value",
)
}
else -> null
}
}
/**
* Fetch any alteration related bonus and build a ThrowsCardUio.Detail for each.
*/
fun List<Alteration.Status>?.alterationsBonus(): List<NetworkThrow.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,
emphasis = dice.emphasis,
fail = status.fail,
critical = status.critical,
)
NetworkThrow.Detail(
title = status.name,
throws = NetworkThrow.Throw(
dice = dice.icon,
advantage = dice.advantage,
disadvantage = dice.disadvantage,
emphasis = dice.emphasis,
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<NetworkThrow.Detail> {
return this?.mapNotNull { status ->
status.bonus?.let { bonus ->
allValue.add(bonus.value)
NetworkThrow.Detail(
title = status.name,
result = "${bonus.value}",
)
}
} ?: emptyList()
}
/**
* Make a roll of dices.
* @param amount the number of dice.
* @param faces the number of faces on the dices.
* @param advantage does the throws have advantage (roll 2 keep max)
* @param disadvantage does the throws have disadvantage (roll 2 keep min)
* @param emphasis does the throw have emphasis (roll 2, keep furthest away from 10)
* @param fail force the result to be 1
* @param critical force the result to amount.
*/
fun roll(
amount: Int = 1,
faces: Int = 20,
advantage: Boolean = false,
disadvantage: Boolean = false,
emphasis: Boolean,
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) },
)
}
!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 { min(it.first, it.second) },
)
}
!advantage && !disadvantage && emphasis -> {
val roll = List(amount) {
var first: Int
var second: Int
do {
first = random(faces = faces, fail = fail, critical = critical)
second = random(faces = faces, fail = fail, critical = critical)
} while (abs(first - 10) == abs(second - 10))
if (abs(first - 10) < abs(second - 10)) {
second to first
} else {
first to second
}
}
DiceRollResult(
label = roll.joinToString(" + ") { "${it.first}~${it.second}" },
value = roll.sumOf { it.first },
)
}
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 -> Random.nextInt(from = 1, until = faces + 1)
}
}
private data class DiceRollResult(
val label: String,
val value: Int,
)
}