Skip to content

Commit

Permalink
[#46] Add Resurrect Crops option to herb calculator
Browse files Browse the repository at this point in the history
Adds a new option to the herb calculator for casting Resurrect Crops on all herbs that die. This calculates the increase in survival chance due to the spell (and subsequent increase in yield), as well as the cost of the runes for the spell.
  • Loading branch information
LucasPickering committed Dec 8, 2021
1 parent f011c78 commit 6d22584
Show file tree
Hide file tree
Showing 12 changed files with 419 additions and 82 deletions.
96 changes: 68 additions & 28 deletions src/commands/calc/farm/herb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ use crate::{
utils::{
context::CommandContext,
farm::{Herb, PatchStats},
fmt,
hiscore::HiscorePlayer,
fmt, hiscore,
magic::Spell,
skill::Skill,
},
};
Expand All @@ -26,18 +26,24 @@ use strum::IntoEnumIterator;
pub struct CalcFarmHerbCommand {
/// Farming level (affects crop yield). If provided, this will override
/// hiscores lookup for a player.
#[structopt(short = "l", long = "lvl")]
#[structopt(short = "l", long = "lvl", alias = "level", alias = "farm")]
farming_level: Option<usize>,

/// The player to pull a farming level from. If not given, will use the
/// default player in the config.
/// Magic level (affects Resurrect Crops success rate). If provided, this
/// will override hiscores lookup for a player.
#[structopt(long = "magic-lvl", alias = "magic")]
magic_level: Option<usize>,

/// The player to pull levels from. If not given, will use the default
/// player in the config.
#[structopt(short, long)]
player: Vec<String>,
}

impl Command for CalcFarmHerbCommand {
fn execute(&self, context: &CommandContext) -> anyhow::Result<()> {
let herb_cfg = &context.config().farming.herbs;
let cfg = context.config();
let herb_cfg = &cfg.farming.herbs;

// Make sure at least one patch is configured
if herb_cfg.patches.is_empty() {
Expand All @@ -54,31 +60,56 @@ impl Command for CalcFarmHerbCommand {
// 2. --player param
// 3. Default player in config
// 4. Freak out
let farming_level = match self {
Self {
farming_level: Some(farming_level),
..
} => *farming_level,
Self {
farming_level: None,
player,
} => {
// This error message isn't the best, but hopefully it gets the
// point across
let username = context
.config()
.get_username(player)
.context("Error loading farming level")?;
let player = HiscorePlayer::load(&username)?;
player.skill(Skill::Farming).level
let farming_level = hiscore::get_level_from_args(
cfg,
Skill::Farming,
&self.player,
self.farming_level,
)
.context("Error getting farming level")?;

// If the user wants to use Resurrect Crops, grab their magic level.
// This will error out if we can't get a level, so we only do it when
// actually needed
let magic_level = if herb_cfg.resurrect_crops {
let level =
// We use the same logic as grabbing farming level
hiscore::get_level_from_args(
cfg,
Skill::Magic,
&self.player,
self.magic_level,
)
.context("Error getting magic level")?;

// Make sure the player can actually cast the spell
let required_level = Spell::ResurrectCrops.level();
if level < required_level {
return Err(OsrsError::InvalidConfig(format!(
"Resurrect Crops requires level {}, \
but player has level {}",
required_level, level
))
.into());
}

Some(level)
} else {
None
};

// Print a little prelude to give the user some info
println!("Farming level: {}", farming_level);
// Only print magic level if it's being used
if let Some(magic_level) = magic_level {
println!("Magic level: {}", magic_level);
}
println!("{}", &herb_cfg);
println!();
println!("Survival chance is an average across all patches. Yield values take into account survival chance.");
println!(
"Survival chance is an average across all patches.\
Yield values take into account survival chance."
);

let mut table = Table::new();
table.set_format(
Expand All @@ -99,8 +130,12 @@ impl Command for CalcFarmHerbCommand {
for herb in Herb::iter() {
// Don't show herbs that the user doesn't have the level to grow
if herb.farming_level() <= farming_level {
let herb_stats =
calc_total_patch_stats(farming_level, herb_cfg, herb)?;
let herb_stats = calc_total_patch_stats(
farming_level,
magic_level,
herb_cfg,
herb,
)?;

// TODO add row highlighting for best rows by profit
table.add_row(row![
Expand All @@ -127,6 +162,7 @@ impl Command for CalcFarmHerbCommand {
/// per-run expected output.
fn calc_total_patch_stats(
farming_level: usize,
magic_level: Option<usize>,
herb_cfg: &FarmingHerbsConfig,
herb: Herb,
) -> anyhow::Result<PatchStats> {
Expand All @@ -137,8 +173,12 @@ fn calc_total_patch_stats(
// HerbPatch value, which picks out only the relevant modifiers. This
// makes it easy to pass around the modifier context that we need, and
// nothing more.
let patch_stats =
patch.calc_patch_stats(farming_level, herb_cfg, herb)?;
let patch_stats = patch.calc_patch_stats(
farming_level,
magic_level,
herb_cfg,
herb,
)?;

// We aggregate survival chance here, then we'll turn it into an
// average below
Expand Down
5 changes: 3 additions & 2 deletions src/commands/calc/xp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,9 @@ impl CalcXpCommand {
player,
skill: Some(skill),
} => {
let player = HiscorePlayer::load(
&context.config().get_username(player)?,
let player = HiscorePlayer::load_from_args(
context.config(),
player.as_slice(),
)?;
Ok(player.skill(*skill).xp)
}
Expand Down
5 changes: 5 additions & 0 deletions src/commands/config/set_herb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ impl Command for ConfigSetHerbCommand {
"Bottomless bucket?",
current_herb_config.bottomless_bucket,
)?;
let resurrect_crops = console::confirm(
"Resurrect crops?",
current_herb_config.resurrect_crops,
)?;
let compost = console::enum_select::<Compost>(
"Compost",
current_herb_config.compost,
Expand Down Expand Up @@ -91,6 +95,7 @@ impl Command for ConfigSetHerbCommand {
bottomless_bucket,
farming_cape,
magic_secateurs,
resurrect_crops,
compost,
anima_plant,

Expand Down
5 changes: 2 additions & 3 deletions src/commands/hiscore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@ pub struct HiscoreCommand {

impl Command for HiscoreCommand {
fn execute(&self, context: &CommandContext) -> anyhow::Result<()> {
let username =
context.config().get_username(self.username.as_slice())?;
let player = HiscorePlayer::load(&username)?;
let player =
HiscorePlayer::load_from_args(context.config(), &self.username)?;

// Print a table for skills
println!("Skills");
Expand Down
35 changes: 8 additions & 27 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
use crate::{
error::OsrsError,
utils::{
diary::AchievementDiaryLevel,
farm::{AnimaPlant, Compost, HerbPatch},
},
use crate::utils::{
diary::AchievementDiaryLevel,
farm::{AnimaPlant, Compost, HerbPatch},
};
use anyhow::Context;
use figment::{
Expand Down Expand Up @@ -38,15 +35,17 @@ pub struct FarmingHerbsConfig {
/// The list of herb patches being farmed
pub patches: Vec<HerbPatch>,

// Global-level modifiers, that apply to all patches
/// The type of compost being used
pub compost: Option<Compost>,
/// Do you have magic secateurs equipped? (10% yield bonus)
pub magic_secateurs: bool,
/// Do you have a farming cape equipped? (5% yield bonus)
pub farming_cape: bool,
/// Do you have a bottomless bucket? Affects cost of compost per patch
pub bottomless_bucket: bool,
/// Do you use the Resurrect Crops spell on patches that die?
pub resurrect_crops: bool,
// Global-level modifiers, that apply to all patches
/// The type of compost being used
pub compost: Option<Compost>,
/// The type of Anima plant currently alive at the Farming Guild (can
/// affect disease and yield rates)
pub anima_plant: Option<AnimaPlant>,
Expand Down Expand Up @@ -125,22 +124,4 @@ impl OsrsConfig {
format!("Error writing config to file `{}`", path.display())
})
}

/// Convert a (possibly empty) list of username parts into a username. If
/// the array has at least one element, the elements will be appended
/// together with spaces between. If not, then we'll fall back to the
/// default player defined in the config. If that is not present either,
/// then return an arg error.
pub fn get_username(&self, username: &[String]) -> anyhow::Result<String> {
match (username, &self.default_player) {
// No arg provided, empty default - error
(&[], None) => {
Err(OsrsError::ArgsError("No player given".into()).into())
}
// No arg provided, but we have a default - use the default
(&[], Some(default_player)) => Ok(default_player.clone()),
// Arg was provided, return that
(&[_, ..], _) => Ok(username.join(" ")),
}
}
}
3 changes: 3 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ pub enum OsrsError {
#[error("Invalid level. Must be between 1 and 127, got: {0}")]
InvalidLevel(usize),

#[error("Invalid configuration: {0}")]
InvalidConfig(String),

#[error("Missing configuration for field `{key}`. {message}")]
Unconfigured { key: String, message: String },
}
Loading

0 comments on commit 6d22584

Please sign in to comment.