diff --git a/Cargo.lock b/Cargo.lock index 4b83825..52c17c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1093,7 +1093,7 @@ dependencies = [ [[package]] name = "fren" -version = "2.5.1" +version = "2.6.0" dependencies = [ "axum 0.8.1", "base64 0.22.1", diff --git a/Cargo.toml b/Cargo.toml index 51d8a41..1080ea3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fren" -version = "2.5.1" +version = "2.6.0" edition = "2024" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/src/discord/improvements.rs b/src/discord/improvements.rs index 771ff35..bc8b7de 100644 --- a/src/discord/improvements.rs +++ b/src/discord/improvements.rs @@ -15,22 +15,33 @@ pub async fn improvements(ctx: Context<'_>) -> Result<(), Error> { msg_builder.push_line("Anything can be improved with enough Fren Coins!"); msg_builder.push_line(""); + let improvements = Improvements::get_improvement_config(&ctx.data().db)?; + for improvement_type in ImprovementType::iter() { msg_builder.push("* "); - if let Some(improvement) = Improvements::get_improvement(&ctx.data().db, improvement_type)? - { - if improvement_type.has_levels() { - msg_builder.push(format!( - "({} FC Level={}) ", - improvement_type.price().separate_with_commas(), - improvement.level - )); + + if let Some(improvement) = improvements.get_improvement(&improvement_type) { + if improvement.has_at_least_one_level() { + if improvement_type.has_levels() { + msg_builder.push(format!( + "({}/{} FC Level={}) ", + improvement.funding_amount.separate_with_commas(), + improvement_type.price().separate_with_commas(), + improvement.level + )); + } else { + msg_builder.push(" (✅) "); + } } else { - msg_builder.push(" (✅) "); + msg_builder.push(format!( + "({}/{} FC) ", + improvement.funding_amount.separate_with_commas(), + improvement_type.price().separate_with_commas(), + )); } } else { msg_builder.push(format!( - "({} FC) ", + "(0/{} FC) ", improvement_type.price().separate_with_commas() )); } @@ -51,13 +62,14 @@ pub async fn improvements(ctx: Context<'_>) -> Result<(), Error> { #[poise::command(prefix_command, guild_only, category = "Improvements")] pub async fn buy_improvement( ctx: Context<'_>, - #[description = "Improvement to buy"] + #[min = 0u32] + #[description = "Amount to contribute"] + amount: u32, + #[description = "Improvement to contribute to"] #[rest] improvement_type: ImprovementType, ) -> Result<(), Error> { - if Improvements::get_improvement(&ctx.data().db, improvement_type)?.is_some() - && !improvement_type.has_levels() - { + if Improvements::check_is_completed(&ctx.data().db, improvement_type)? { ctx.reply("Sorry, that improvement has already been purchased.") .await?; return Ok(()); @@ -69,9 +81,18 @@ pub async fn buy_improvement( improvement_type.price() as u32, )?; - Improvements::increment_improvement(&ctx.data().db, improvement_type)?; + let (has_improved, _) = + Improvements::contribute_to_improvement(&ctx.data().db, improvement_type, amount as u64)?; - ctx.reply(improvement_type.post_buy_description()).await?; + if has_improved { + ctx.reply(improvement_type.post_buy_description()).await?; + } else { + ctx.reply(format!( + "Thank you for contributing to {}", + improvement_type.name() + )) + .await?; + } Ok(()) } @@ -79,9 +100,8 @@ pub async fn buy_improvement( /// View Friendship Tower and all its glory!!! #[poise::command(prefix_command, guild_only, category = "Improvements")] pub async fn friendship_tower(ctx: Context<'_>) -> Result<(), Error> { - if let Some(tower) = - Improvements::get_improvement(&ctx.data().db, ImprovementType::FriendshipTower)? - { + let improvements = Improvements::get_improvement_config(&ctx.data().db)?; + if let Some(tower) = improvements.get_improvement(&ImprovementType::FriendshipTower) { let buffer = 2; let pic_height = tower.level as usize + buffer + 1; let width = 5; @@ -116,13 +136,13 @@ pub async fn friendship_tower(ctx: Context<'_>) -> Result<(), Error> { /// See how big Jotchua's college fund is! #[poise::command(prefix_command, guild_only, category = "Improvements")] pub async fn jotchua_college_fund(ctx: Context<'_>) -> Result<(), Error> { - let funds_level = if let Some(fund) = - Improvements::get_improvement(&ctx.data().db, ImprovementType::JotchuaCollegeFund)? - { - fund.level - } else { - 0 - }; + let improvements = Improvements::get_improvement_config(&ctx.data().db)?; + let funds_level = + if let Some(fund) = improvements.get_improvement(&ImprovementType::JotchuaCollegeFund) { + fund.level + } else { + 0 + }; let college_savings = funds_level as u64 * ImprovementType::JotchuaCollegeFund.price(); diff --git a/src/migrations/migration_10_add_funding_amount.rs b/src/migrations/migration_10_add_funding_amount.rs new file mode 100644 index 0000000..097592c --- /dev/null +++ b/src/migrations/migration_10_add_funding_amount.rs @@ -0,0 +1,49 @@ +use j_db::database::Database; +use j_db::migration::Migration; +use json::JsonValue; + +pub struct Migration10AddFundingAmount {} + +impl Migration for Migration10AddFundingAmount { + fn up(&self, db: &Database) -> j_db::error::Result<()> { + let tree = db.db.open_tree("Improvements")?; + + for improvement_entry in tree.iter() { + let (id, improvement_bytes) = improvement_entry?; + + let mut improvement_cfg = + json::parse(std::str::from_utf8(&improvement_bytes).unwrap())?; + + for (_, improvement) in improvement_cfg["improvements"].entries_mut() { + improvement["funding_amount"] = JsonValue::Number(json::number::Number::from(0)); + } + + tree.insert(id, improvement_cfg.to_string().into_bytes())?; + } + + Ok(()) + } + + fn down(&self, db: &Database) -> j_db::error::Result<()> { + let tree = db.db.open_tree("Improvements")?; + + for improvement_entry in tree.iter() { + let (id, improvement_bytes) = improvement_entry?; + + let mut improvement_cfg = + json::parse(std::str::from_utf8(&improvement_bytes).unwrap())?; + + for improvement in improvement_cfg["improvements"].members_mut() { + improvement.remove("funding_amount"); + } + + tree.insert(id, improvement_cfg.to_string().into_bytes())?; + } + + Ok(()) + } + + fn version(&self) -> u64 { + 10 + } +} diff --git a/src/migrations/mod.rs b/src/migrations/mod.rs index 7348b12..8620c75 100644 --- a/src/migrations/mod.rs +++ b/src/migrations/mod.rs @@ -4,6 +4,7 @@ use crate::migrations::migration_6_add_social_credit::Migration6AddSocialCredit; use crate::migrations::migration_7_flip_bounds::Migration7FlipBounds; use crate::migrations::migration_8_fix_metadata_id::Migration8FixMetadataId; use crate::migrations::migration_9_update_to_emp::Migration9UpdateToEMP; +use crate::migrations::migration_10_add_funding_amount::Migration10AddFundingAmount; use crate::migrations::migration2_remove_imgur::Migration2RemoveImgur; use crate::migrations::migration3_remove_img::Migration3RemoveImage; use j_db::database::Database; @@ -12,6 +13,7 @@ use j_db::migration::Direction; mod migration2_remove_imgur; mod migration3_remove_img; +mod migration_10_add_funding_amount; mod migration_4_update_random; mod migration_5_update_motivation; mod migration_6_add_social_credit; @@ -19,7 +21,7 @@ mod migration_7_flip_bounds; mod migration_8_fix_metadata_id; mod migration_9_update_to_emp; -pub const CURRENT_DB_VERSION: u64 = 9; +pub const CURRENT_DB_VERSION: u64 = 10; #[allow(clippy::single_match)] pub fn do_migration(db: &Database) { @@ -80,6 +82,12 @@ pub fn do_migration(db: &Database) { Direction::Up, ) .unwrap(), + 10 => migration::do_migration::( + db, + Migration10AddFundingAmount {}, + Direction::Up, + ) + .unwrap(), _ => {} } } diff --git a/src/models/gogurt_reserves.rs b/src/models/gogurt_reserves.rs index c1760d6..97da219 100644 --- a/src/models/gogurt_reserves.rs +++ b/src/models/gogurt_reserves.rs @@ -84,7 +84,7 @@ impl GogurtReserves { } pub fn has_night_market(db: &Database) -> Result { - Ok(Improvements::get_improvement(db, ImprovementType::GogurtNightMarket)?.is_some()) + Improvements::check_is_completed(db, ImprovementType::GogurtNightMarket) } pub fn get_next_market_update_time(db: &Database) -> Result, Error> { diff --git a/src/models/improvements.rs b/src/models/improvements.rs index 998781e..514a797 100644 --- a/src/models/improvements.rs +++ b/src/models/improvements.rs @@ -1,6 +1,7 @@ use crate::error::Error; use j_db::database::Database; use j_db::model::JdbModel; +use log::info; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fmt::{Debug, Display, Formatter}; @@ -105,6 +106,13 @@ impl FromStr for ImprovementType { #[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq)] pub struct Improvement { pub level: u32, + pub funding_amount: u64, +} + +impl Improvement { + pub fn has_at_least_one_level(&self) -> bool { + self.level > 0 + } } #[derive(Debug, Deserialize, Serialize, Clone)] @@ -121,40 +129,71 @@ impl Improvements { })) } - pub fn get_improvement( - db: &Database, - improvement_type: ImprovementType, - ) -> Result, Error> { - let improvements = Self::get_improvement_config(db)?; - - Ok(improvements.improvements.get(&improvement_type).cloned()) - } - - pub fn increment_improvement( - db: &Database, - improvement_type: ImprovementType, - ) -> Result { - let mut improvements = Self::get_improvement_config(db)?; - - let entry = improvements + pub fn get_improvement_mut(&mut self, improvement_type: ImprovementType) -> &mut Improvement { + (self .improvements .entry(improvement_type) - .or_insert(Improvement { level: 0 }); + .or_insert(Improvement { + level: 0, + funding_amount: 0, + })) as _ + } - entry.level += 1; + pub fn get_improvement(&self, improvement_type: &ImprovementType) -> Option<&Improvement> { + self.improvements.get(improvement_type) + } - let level = entry.level; + pub fn contribute_to_improvement( + db: &Database, + improvement_type: ImprovementType, + amount: u64, + ) -> Result<(bool, u32), Error> { + let mut improvements = Self::get_improvement_config(db)?; + + let improvement = improvements.get_improvement_mut(improvement_type); + + improvement.funding_amount += amount; + + let has_improved = if improvement.funding_amount >= improvement_type.price() { + let levels_to_fund = improvement.funding_amount / improvement_type.price(); + info!( + "Increasing improvement {} by {} levels", + improvement_type.name(), + levels_to_fund, + ); + + improvement.level += levels_to_fund as u32; + improvement.funding_amount -= improvement_type.price() * levels_to_fund; + true + } else { + false + }; + + let level = improvement.level; db.insert(improvements)?; - Ok(level) + Ok((has_improved, level)) } - pub fn has_improvement( + pub fn has_improvement(&self, improvement_type: ImprovementType) -> bool { + if let Some(improvement) = self.get_improvement(&improvement_type) { + improvement.has_at_least_one_level() + } else { + false + } + } + + pub fn is_completed(&self, improvement_type: ImprovementType) -> bool { + self.has_improvement(improvement_type) && !improvement_type.has_levels() + } + + pub fn check_is_completed( db: &Database, improvement_type: ImprovementType, ) -> Result { - Ok(Self::get_improvement(db, improvement_type)?.is_some()) + let improvements = Improvements::get_improvement_config(db)?; + Ok(improvements.is_completed(improvement_type)) } } diff --git a/src/models/lil_fren.rs b/src/models/lil_fren.rs index 202fa19..2995a61 100644 --- a/src/models/lil_fren.rs +++ b/src/models/lil_fren.rs @@ -338,7 +338,7 @@ impl LilFren { } if lil_fren.hunger < 0.25 - && Improvements::has_improvement(db, ImprovementType::LilBuddyAutoFeeder)? + && Improvements::check_is_completed(db, ImprovementType::LilBuddyAutoFeeder)? { lil_fren.hunger = 0.25; }