njlyon0 / dndR

Dungeons & Dragons Functions for Players and Dungeon Masters
https://njlyon0.github.io/dndR/
Other
16 stars 2 forks source link

Expand roll function #13

Closed HumbertoNappo closed 6 months ago

HumbertoNappo commented 7 months ago

As my sorcerer levels up, I need more dice to roll for damage, however, the roll functions in dndR (and pretty much all other dice simulations I know) fall short for this character because of two features: Elemental Adept and Empowered Spell. The first one turns all ones into twos and the second one allows for rerolling a number of dice. Writing a function that rolls dice replicating the statistical effect of Elemental Adept is quite straightforward (just used sample(2,2:6) for a d6) but I could not think of a generic way to do this in the roll() function. Adding support for discretionary rerolls, however, seems to be very tricky. I wrote a pretty lackluster function to do the rerolls by sorting the results of a previous roll and rerolling the first n dice. However, this was done in a very inelegant way. Maybe a nice way to allow for discretionary rerolls would be to have a logical argument that when TRUE would prompt a question asking how many dice should be rerolled? I don't know, just throwing ideas.

I managed to expand the roll function to allow single rerolls of some predetermined results (as in the Great Weapon Fighting fighting style) but I could not add the features I needed. I added this modification to the roll() function for the sake of simplicity, but I think you would likely prefer to do so in the specific functions d2(), d3(), d4()...

new_roll_function <- function (dice = "d20", show_dice = FALSE, reroll = NULL) 
  {
    if (!is.character(dice)) 
      stop("`dice` must be specified as a character (e.g., '3d8')")
    dice_count <- base::gsub(pattern = "d", replacement = "", 
                             x = stringr::str_extract(string = dice, pattern = "[:digit:]{1,10000000}d"))
    if (base::nchar(dice_count) == 0 | is.na(dice_count)) {
      base::message("Number of dice unspecified, assuming 1")
      dice_count <- 1
    }
    dice_type <- stringr::str_extract(string = dice, pattern = "d[:digit:]{1,3}")
    if (!dice_type %in% c("d2", "d3", "d4", "d6", "d8", "d10", 
                          "d12", "d20", "d100")) 
      stop("Dice type not recognized")
    dice_result <- base::list()
    if (dice_type == "d2") {
      for (k in 1:dice_count) {
        dice_result[[k]] <- base::data.frame(result = d2())
        ifelse(is.null(reroll) == FALSE & dice_result[[k]] %in% reroll,
               dice_result[[k]] <- base::data.frame(result = d2()),
               NA)
      }
    }
    if (dice_type == "d3") {
      for (k in 1:dice_count) {
        dice_result[[k]] <- base::data.frame(result = d3())
        ifelse(is.null(reroll) == FALSE & dice_result[[k]] %in% reroll,
               dice_result[[k]] <- base::data.frame(result = d3()),
               NA)
      }
    }
    if (dice_type == "d4") {
      for (k in 1:dice_count) {
        dice_result[[k]] <- base::data.frame(result = d4())
        ifelse(is.null(reroll) == FALSE & dice_result[[k]] %in% reroll,
               dice_result[[k]] <- base::data.frame(result = d4()),
               NA)
      }
    }
    if (dice_type == "d6") {
      for (k in 1:dice_count) {
        dice_result[[k]] <- base::data.frame(result = d6())
        ifelse(is.null(reroll) == FALSE & dice_result[[k]] %in% reroll,
               dice_result[[k]] <- base::data.frame(result = d6()),
               NA)
      }
    }
    if (dice_type == "d8") {
      for (k in 1:dice_count) {
        dice_result[[k]] <- base::data.frame(result = d8())
        ifelse(is.null(reroll) == FALSE & dice_result[[k]] %in% reroll,
               dice_result[[k]] <- base::data.frame(result = d8()),
               NA)
      }
    }
    if (dice_type == "d10") {
      for (k in 1:dice_count) {
        dice_result[[k]] <- base::data.frame(result = d10())
        ifelse(is.null(reroll) == FALSE & dice_result[[k]] %in% reroll,
               dice_result[[k]] <- base::data.frame(result = d10()),
               NA)
      }
    }
    if (dice_type == "d12") {
      for (k in 1:dice_count) {
        dice_result[[k]] <- base::data.frame(result = d12())
        ifelse(is.null(reroll) == FALSE & dice_result[[k]] %in% reroll,
               dice_result[[k]] <- base::data.frame(result = d12()),
               NA)
      }
    }
    if (dice_type == "d20" & dice_count != 2) {
      for (k in 1:dice_count) {
        dice_result[[k]] <- base::data.frame(result = d20())
        ifelse(is.null(reroll) == FALSE & dice_result[[k]] %in% reroll,
               dice_result[[k]] <- base::data.frame(result = d20()),
               NA)
      }
    }
    if (dice_type == "d100") {
      for (k in 1:dice_count) {
        dice_result[[k]] <- base::data.frame(result = d100())
        ifelse(is.null(reroll) == FALSE & dice_result[[k]] %in% reroll,
               dice_result[[k]] <- base::data.frame(result = d100()),
               NA)
      }
    }
    dice_result_df <- purrr::list_rbind(x = dice_result)
    total <- base::sum(dice_result_df$result, na.rm = TRUE)
    if (dice_type == "d20" & dice_count == 2) {
      total <- base::data.frame(roll_1 = d20(), roll_2 = d20())
      base::message("Assuming you're rolling for (dis)advantage so both rolls returned")
    }
    if (show_dice == TRUE & dice_count > 1 & dice != "2d20") {
      base::message("Individual rolls: ", paste(sort(dice_result_df$result), 
                                                collapse = ", "))
    }
    return(total)
  }
njlyon0 commented 7 months ago

This is a great idea and thank you for taking a first stab at the tweaks to the roll function! I definitely think this is worth building into the package.

I like the idea of making it conditional on some sort of logical argument in roll. It lets the simpler dice functions (e.g., d6, etc.) remain really simple while still providing the tools you point out. I think a "secret" re-roll function would be a neat way of handling this without copy/pasting the required code.

I'm jamming on some pre-holiday work at my job but I think over the holidays I should have some time to get going on this. Thanks for the idea and I'll be in touch when I've made some progress. Also feel free to ping me here if you have a brain blast about other improvements to this re-rolling funcitonality

njlyon0 commented 6 months ago

Okay, I've modified the roll function to include a logical re_roll argument. If set to TRUE, any 1s from the first will be re-rolled once and the second result is retained. This should be easily modifiable to include re-rolling 2s as well (as in Great Weapon Fighting).

For Elemental Adept I could see counting up the number of ones in your first roll and mentally changing them to twos being easier than modifying function code (in any dice simulator).

Empowered Spell honestly seems like kind of a pain to include because it does require a judgement call every time. I think an easier solution than making the roll function become much more complicated is just to manually re-use the roll function for the specified number of dice. For example, if you roll 6d6 and get four results you're happy with, you would do a second roll call with only the two remaining dice. In effect "re-rolling" though you'd need to drop the re-rolling dice' results from the first roll and add the second roll's results manually.

All that said, I'm considering adding an argument for whether the player has Great Weapon Fighting or Elemental Adept and building an argument in that does the specific tweaks required by either of those features. My main hesitation is just that there are likely several dozen such feats that slightly modify roll results in different ways and accounting for all of them could quickly get out of hand.

Anyway, I'm nearly done with this fix for the moment.

njlyon0 commented 6 months ago

I'm going to close this issue and leave the reroll as a logical to automatically re-roll 1s for now. I'm going to ask around my D&D community to solicit opinions and may re-open this issue (or create a new related one) depending on what feedback I get from others.